-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Open-Closed Principle
- 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 원칙
탬플릿
- 바뀌는 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법
초난감 DAO
JDBC 수정 기능의 예외처리 코드
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
} catch (SQLException e) {
throw e;
} finally {
if(ps != null){
try{
ps.close();
} catch(SQLException e) { }
}
if(c != null){
try{
c.close();
} catch(SQLException e) { }
}
}
}- 예외상황에서도 안전한 코드가 됐다.
- finally는 try 블록을 수행한 후에 예외가 발생하든 발생하지 않든 상관없이 반드시 실행되는 코드를 넣을 때 사용한다.
- close() 메서드도 SQLException이 발생할 수 있으므로 try/catch 문으로 처리해줘야 한다.
- 예외가 발생한 경우에 보통 로그를 남기는 등의 부가작업이 필요할 수 있으므로 catch 블록은 일단 만들어두는 편이 좋다.
- 여기서 get(), add() 등 DB에 접근해서 데이터를 가져오거나 데이터를 저장하는 로직이 새로 추가될 때마다 위의 try/catch/finally 문을 매번 작성해야 하는 문제점이 있다.
변하는 것과 변하지 않는 것
JDBC try/catch/finally 코드의 문제점
- deleteAll() 같은 형태의 수많은 메서드를 만든다고 해보자.
- 복사 붙여넣기를 열심히 해서 수백 개를 만들었는데, 갑자기 리소스를 닫는 곳에서 예외가 발생했을 때 로그를 남기라는 요구사항이 들어왔다.
- 수백 개를 모두 찾아서 완벽하게 할 자신이 있는가..?
- 그리고 만약 Connection.close() 호출을 하지 않았다면 리소스를 돌려주지 못해 리소스 부족 현상이 발생할 수 있다.
- 이런 문제를 효과적으로 다루기 위해서는 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 것이다.
분리와 재사용을 위한 디자인 패턴 적용
- 가장 먼저 할 일은 변하는 성격이 다른 것을 찾아내는 것이다.
개선할 deleteAll() 메서드
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = c.preparedStatement("delete from members"); // 여기만 변하는 부분, 나머지는 변하지 않음
ps.executeUpdate();
} catch (SQLException e){
throw e;
} finally {
if(ps != null) { try { ps.close(); } catch (SQLException e) {} }
if(c != null) { try { c.close(); } catch (SQLException e) {} }
}- PreparedStatement를 만들어서 업데이트용 쿼리를 실행하는 메서드라면 deleteAll() 메서드와 구조는 거의 비슷할 것이다.
- 비슷한 기능의 메서드에서 동일하게 나타날 수 있는 변하지 않고 고정되는 부분과, 각 메서드마다 로직에 따라 변하는 부분을 위와 같이 구분할 수 있다.
해결 방법 : 메서드 추출
-
변하는 부분을 메서드로 빼는 것이다.
-
변하지 않는 부분이 변하는 부분을 감싸고 있어서 변하지 않는 부분을 추출하기가 어려워 보이기 때문에 여기서는 반대로 했다.
-
수정된 deleteAll()
public void deleteAll() throws SQLException { ... try { c = dataSource.getConnection(); ps = makeStatement(c); // 변하는 부분을 메서드로 추출하고 변하지 않는 부분에서 호출 ps.executeUpdate(); } catch (SQLException e) { } ... } private PreparedStatement makeStatement(Connection c) throws SQLException { PreparedStatement ps; ps = c.preparedStatement("delete from users"); return ps; }
- 보통 메서드 추출 리팩토링을 적용한 경우에는 분리시킨 메서드를 다른 곳에서 재사용할 수 있어야 한다.
- 여기서는 반대로 분리시키고 남은 메서드가 재사용이 필요한 부분이고, 분리된 메서드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문에 큰 이득이 없어 보인다.
해결 방법 : 템플릿 메서드 패턴의 적용
- 템플릿 메서드 패턴은 변하지 않는 부분은 슈퍼클래스에 두고, 변하는 부분은 추상 메서드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.
public abstract class UserDao {
abstract protected PreparedStatement makeStatement(Connection c) throws
SQLException;
}
public class UserDaoDeleteAll extends UserDao {
abstract protected PreparedStatement makeStatement(Connection c)
throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}- DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 하는 문제점이 있다.
- 이 방식을 사용한다면 UserDao의 JDBC 메서드가 4개일 경우 4개의 서브클래스를 만들어야 함.
- 이 경우 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다.
- 즉, 서브클르스들이 이미 클레스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있다.
- 그 관계에 대한 유연성이 떨어져 버린다.
- 상속을 통해 확장을 꾀하는 템플릿 메서드 패턴의 단점이 고스란히 드러난다.
해결 방법 : 전략 패턴
-
오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 전략 패턴을 사용해보자
-
전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.
-
StatementStrategy 인터페이스
public interface StatementStrategy { PreparedStatement makePreparedStatement(Connection c) throws SQLException; } public class DeleteAllStatement implements StatementStrategy { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.preparedStatement("delete from users"); } }
-
전략 패턴을 따라 DeleteAllStatement가 적용된 deleteAll() 메서드
public void deleteAll() throws SQLException { ... try { c = dataSource.getConnection(); StatementStrategy strategy = new DeleteAllStatement(); ps = strategy.makePreparedStatement(c); } catch (SQLException e) { ... } ... }
- 전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있는 것이다.
- 하지만 여기선 컨텍스트 안에서 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다.
- 컨텍스트가 StatementStrategy 인터페이스뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는 건, 전략 패턴에도 OCP에도 잘 들어맞는다고 할 수 없다.
DI 적용을 위한 클라이언트/컨텍스트 분리
- 전략 패턴에서는 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 클라이언트가 결정하는 것이 일반적
- 클라이언트가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다.
- DI란 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조
- 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다.
-
deleteAll() 메서드에서 다음 코드는 클라이언트에 들어가야 할 코드다.
StatementStrategy strategy = new DeleteAllStatement();
-
클라이언트는 DeleteAllStatement 와 같은 전략 클래스의 오브젝트를 컨텍스트의 메서드를 호출하며 전달해야 한다.
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection c = null; PreparedStatement ps = null; try { c = dataSource.getConnection(); ps = stmt.makePreparedStatement(c); ps.executeUpdate(); } catch (SQLException e) { throw e; } finally { // 리소스 정리 } }
- 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.
- 제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에서 호출해서 사용하면 된다.
-
클라이언트 책임을 담당할 deleteAll()
public void deleteAll() throws SQLException { StatementStrategy st = new DeleteAllStatement(); // 전략, 호출 전에 생성 jdbcContextWithStatementStrategy(st); }
-
JDBC 전략 패턴 최적화
로컬 클래스
- StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리는 것이다.
- 특정 메서드에서만 사용되는 것이라면 아래와 같이 로컬 클래스로 만들 수도 있다.
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy {
// add 메서드 내부에서만 사용할 로컬 클래스
User user;
public AddStatement(User user){ this.user = user; }
public PreparedStatement makePreparedStatement(Connection c) throws
SQLException {
// ...
}
}
StatementStrategy st = new AddStatement(user);
jdbcContextWithStatementStrategy(st);
}- 로컬 클래스는 선언된 메서드 내에서만 사용 가능
- 로컬 클래스는 클래스 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근 가능
- 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야 한다.
참고 : 중첩 클래스의 종류
- 독립적으로 오브젝트로 만들어질 수 있는 static class
- 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 inner class(내부 클래스)
- 내부 클래스는 다시 범위에 따라 3가지로 구분
- 멤버 필드처럼 오브젝트 레벨에 정의되는 멤버 내부 클래스
- 메소드 레벨에 정의되는 로컬 클래스
- 이름을 갖지 않는 익명 내부 클래스
익명 내부 클래스
- 익명 내부 클래스는 선언과 동시에 오브젝트를 생성
- 이름이 없기 때문에 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장 가능
StatementStrategy st = new StatementStrategt() {
public PreparedStatement makePreparedStatement(Connection c)
throws SQLException {
// ..
}
};컨텍스트와 DI
JdbcContext를 DI 받아서 사용하도록 만든 UserDao
빈 의존관계 변경 : 그냥 읽어보기
- 스프링 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적
- 위에서 JdbcContext는 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의마가 있을 뿐이고, 구현 방법이 바꿀 가능성은 거의 없다.
- 그래서 인터페이스를 구현하도록 만들지 않음
- 스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의된다.
- 인터페이스를 사용하지 않고 DI를 적용하는 것은 문제가 있는가?
- DI 개념에 충실히 따르면, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이나믹하게 주입해주는 것이 맞다.
- 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC 라는 개념을 포괄한다.
- 이런 의미에서 JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는 것은 DI의 기본을 따르고 있다고 볼 수 있다.
- JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유
- JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문
- JdbcContext는 변경되는 상태 정보를 갖고 있지 않음
- 싱글톤으로 등록돼서 여러 오브젝트에서 공유해 사용하는 것이 이상적
- JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문
- JdbcContext는 dataSource 프로퍼티를 통해 DataSource를 주입 받는다.
- DI를 위해서는 주입받는 쪽과 주입되는 쪽 양쪽 모두 스프링 빈으로 등록돼야 한다.
- 스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문이다.
- JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문
- 그저 인터페이스를 만들기가 귀찮으니까 그냥 클래스를 사용하자는 건 잘못된 생각(저자)
- 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임
템플릿과 콜백
-
복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에 적합한 구조
→ 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
→ 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다.
- 전략 패턴의 컨텍스트를 템플릿이라고 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.
템플릿/콜백의 동작원리
- 템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름
- 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다.
템플릿/콜백의 특징
- 템플릿/콜백 패턴의 콜백은 보통 단일 메서드 인터페이스를 사용(Functional Interface)
- 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
- 콜백 인터페이스의 메서드에는 보통 파라미터가 있다.
- 이 파라미터는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받을 때 사용
- JdbcContext에서 템플릿 메서드 내에서 생성한 Connection 오브젝트를 콜백 메서드를 실행할 때 파라미터로 넘겨준다.
- 매번 메서드 단위로 사용할 오브젝트를 새롭게 전달받는다는 것이 특징
- 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라잉너트 메서드 내의 정보를 직접 참조한다는 것도 템플릿/콜백의 고유한 특징
- 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다.
템플릿/콜백의 작업 흐름
- 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공
- 만들어진 콜백은 클라이언트가 템플릿의 메서드를 호출할 때 파라미터로 전달
- 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메서드를 호출
- 콜백은 클라이언트 메서드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
- 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.
- 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.
템플릿/콜백의 응용
- 템플릿에 담을 반복되는 작업 흐름은 어떤 것인지 살펴봐야 한다.
- 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지도 생각해봐야 한다.
- 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는 것이 가장 중요하다.
- 그에 따라 콜백 인터페이스를 정의하기 때문이다.
- 코드의 특성이 바뀌는 경계를 잘 살피고 그것을 인터페이스를 사용해 분리한다는, 가장 기본적인 객체지향 원칙에만 충실하면 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있을 것이다.
제네릭스를 이용한 콜백 인터페이스
- 제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메서드를 정의할 수 있다.
스프링 JdbcTemplate
update()
- JdbcTemplate의 콜백은 PreparedStatementCreator 인터페이스의 createPreparedStatement() 메서드
public void deleteAll(){
// jdbcTempalte.update("delete from users");
this.jdbcTemplate.update(new PreparedStatementCreator() {
public PreparedStatement createPreparedStatement(Connection conn)
throws SQLException {
return conn.preparedStatement("delete from users");
}
}
}queryForInt()
-
ResultSetExtractor는 PreparedStatement의 쿼리를 실행해 얻은 ResultSet을 전달 받는 콜백
-
ResultSetExtractor 콜백은 템플릿이 제공하는 ResultSet을 이용해 원하는 값을 추출해서 템플릿에 전달하면, 템플릿은 나머지 작업을 수행한 뒤에 그 값을 query() 메서드의 리턴 값으로 돌려준다.
- 첫 번째 PreparedStatementCreator 콜백은 템플릿으로부터 Connection을 받고 PreparedStatement를 돌려준다.
- 두 번째 ResultSetExtractor는 템플릿으로부터 받은 ResultSet을 받고 거기서 추출한 결과를 돌려준다.
public int getCount(){ // return this.jdbcTemplate.queryForInt("select count(*) from users"); return this.jdbcTemplate.query(new PreparedStatementCreator() { public PreparedStatement createPreparedStatement(Connection con) throws SQLException { return con.prepareStatement("select count(*) from users"); } }, new ResultSetExtractor<Integer>(){ public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { rs.next(); return rs.getInt(1); } }); }
참고하면 좋은 자료
[MySQL 환경의 스프링부트에 하이버네이트 배치 설정 해보기 | 우아한형제들 기술블로그](https://techblog.woowahan.com/2695/)
정리
- 일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다.
- 바뀌지 않는 부분은 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
- 클라이언트 메서드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메서드의 정보를 직접 사용할 수 있어서 편리하다.
- 컨텍스트는 별도의 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해서 사용
- 단일 전략 메서드를 갖는 패턴이라면 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.
- 콜백의 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
- 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용
- 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
- 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야 한다.
템플릿 콜백 패턴 예제
Redisson 설정
@Configuration
public class RedissonConfig {
private final String host = "localhost";
private final String port = "6379";
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress(String.format("redis://%s:%s", host, port));
return Redisson.create(config);
}
}V1
@RequiredArgsConstructor
public abstract class DistributeLock {
private final RedissonClient redissonClient;
public void runTask(String key){
RLock lock = redissonClient.getLock(key);
if (lock.tryLock()) {
try{
successLogic();
}finally {
lock.unlock();
}
} else {
failLogic();
}
}
protected abstract void successLogic();
protected abstract void failLogic();
}
@Component
public class RunTask extends DistributeLock {
@Autowired
public RunTask(RedissonClient redissonClient){
super(redissonClient);
}
public void execute(String key){
super.runTask(key);
}
@Override
protected void successLogic() {
System.out.println("락 잡기 성공!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
protected void failLogic() {
throw new RuntimeException("락 획득 실패");
}
}
@SpringBootTest
public class RedissonTest {
@Autowired
RunTask runTask;
@Test
public void v1Test() throws InterruptedException {
int numberOfThreads = 10;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
service.submit(() -> {
try {
runTask.execute(String.format("lock:1"));
} catch (Exception e){
System.out.println(e.getMessage());
}
latch.countDown();
});
}
latch.await();
}
}V2
@RequiredArgsConstructor
public abstract class DistributeLockV2<T, R> {
private final RedissonClient redissonClient;
public R runTask(String key, T args){
RLock lock = redissonClient.getLock(key);
R result;
if (lock.tryLock()) {
try{
result = successLogic(args);
}finally {
lock.unlock();
}
} else {
result = failLogic();
}
return result;
}
protected abstract R successLogic(T args);
protected abstract R failLogic();
}
@Component
public class RunTaskV2 extends DistributeLockV2<String, String> {
@Autowired
public RunTaskV2(RedissonClient redissonClient){
super(redissonClient);
}
public String execute(String key, String args){
return super.runTask(key, args);
}
@Override
protected String successLogic(String args) {
System.out.println(args);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "success";
}
@Override
protected String failLogic() {
return "fail";
}
}
@SpringBootTest
public class RedissonTest {
@Autowired
RunTaskV2 runTaskV2;
@Test
public void v2Test() throws InterruptedException {
int numberOfThreads = 10;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
int number = i;
service.submit(() -> {
String result = runTaskV2.execute("key", "안녕하세요");
System.out.println("result = " + number + " : " + result);
latch.countDown();
});
}
latch.await();
}
}V3
@Component
@RequiredArgsConstructor
public class DistributeLockV3<T, R> {
private final RedissonClient redissonClient;
public R runTask(String key,
SuccessLogic<T, R> successLogic,
FailLogic failLogic,
T args){
RLock lock = redissonClient.getLock(key);
R result = null;
if (lock.tryLock()) {
try{
result = successLogic.success(args);
}finally {
lock.unlock();
}
} else {
failLogic.fail();
}
return result;
}
}
@FunctionalInterface
public interface FailLogic {
void fail();
}
@FunctionalInterface
public interface SuccessLogic<T, R> {
R success(T args);
}
@Component
@RequiredArgsConstructor
public class TempService implements SuccessLogic<String, Integer>, FailLogic{
private final DistributeLockV3<String, Integer> distributeLockExecutorV3;
public int logic(String name) {
return distributeLockExecutorV3.runTask(name,
this::success,
this::fail,
name);
}
@Override
public void fail() {
System.out.println("실패로직");
}
@Override
public Integer success(String args) {
System.out.println("성공로직");
return 1;
}
}
@SpringBootTest
public class RedissonTest {
@Autowired
TempService tempService;
@Test
public void v3Test() throws InterruptedException {
int numberOfThreads = 10;
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
int number = i;
service.submit(() -> {
Integer result = tempService.logic("key");
System.out.println("result = " + number + " : " + result);
latch.countDown();
});
}
latch.await();
}
}