Skip to content

3장 템플릿 #3

Description

@nsun9505

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

Untitled

빈 의존관계 변경 : 그냥 읽어보기

  • 스프링 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적
    • 위에서 JdbcContext는 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의마가 있을 뿐이고, 구현 방법이 바꿀 가능성은 거의 없다.
    • 그래서 인터페이스를 구현하도록 만들지 않음
  • 스프링의 빈 설정은 클래스 레벨이 아니라 런타임 시에 만들어지는 오브젝트 레벨의 의존관계에 따라 정의된다.
  • 인터페이스를 사용하지 않고 DI를 적용하는 것은 문제가 있는가?
    • DI 개념에 충실히 따르면, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이나믹하게 주입해주는 것이 맞다.
    • 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC 라는 개념을 포괄한다.
      • 이런 의미에서 JdbcContext를 스프링을 이용해 UserDao 객체에서 사용하게 주입했다는 것은 DI의 기본을 따르고 있다고 볼 수 있다.
  • JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유
    1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문
      • JdbcContext는 변경되는 상태 정보를 갖고 있지 않음
      • 싱글톤으로 등록돼서 여러 오브젝트에서 공유해 사용하는 것이 이상적
    2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문
      • JdbcContext는 dataSource 프로퍼티를 통해 DataSource를 주입 받는다.
      • DI를 위해서는 주입받는 쪽과 주입되는 쪽 양쪽 모두 스프링 빈으로 등록돼야 한다.
      • 스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문이다.
  • 그저 인터페이스를 만들기가 귀찮으니까 그냥 클래스를 사용하자는 건 잘못된 생각(저자)
    • 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임

템플릿과 콜백

  • 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부만 자주 바꿔서 사용해야 하는 경우에 적합한 구조

    → 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식

    → 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다.

    • 전략 패턴의 컨텍스트를 템플릿이라고 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.

템플릿/콜백의 동작원리

  • 템플릿은 고정된 작업 흐름을 가진 코드를 재사용한다는 의미에서 붙인 이름
  • 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트를 말한다.

템플릿/콜백의 특징

  • 템플릿/콜백 패턴의 콜백은 보통 단일 메서드 인터페이스를 사용(Functional Interface)
    • 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다.
  • 콜백 인터페이스의 메서드에는 보통 파라미터가 있다.
    • 이 파라미터는 템플릿의 작업 흐름 중에 만들어지는 컨텍스트 정보를 전달받을 때 사용
    • JdbcContext에서 템플릿 메서드 내에서 생성한 Connection 오브젝트를 콜백 메서드를 실행할 때 파라미터로 넘겨준다.
  • 매번 메서드 단위로 사용할 오브젝트를 새롭게 전달받는다는 것이 특징
    • 콜백 오브젝트가 내부 클래스로서 자신을 생성한 클라잉너트 메서드 내의 정보를 직접 참조한다는 것도 템플릿/콜백의 고유한 특징
    • 템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다.

템플릿/콜백의 작업 흐름

  1. 클라이언트의 역할은 템플릿 안에서 실행될 로직을 담은 콜백 오브젝트를 만들고, 콜백이 참조할 정보를 제공
    • 만들어진 콜백은 클라이언트가 템플릿의 메서드를 호출할 때 파라미터로 전달
  2. 템플릿은 정해진 작업 흐름을 따라 작업을 진행하다가 내부에서 생성한 참조정보를 가지고 콜백 오브젝트의 메서드를 호출
    • 콜백은 클라이언트 메서드에 있는 정보와 템플릿이 제공한 참조정보를 이용해서 작업을 수행하고 그 결과를 다시 템플릿에 돌려준다.
  3. 템플릿은 콜백이 돌려준 정보를 사용해서 작업을 마저 수행한다.
    • 경우에 따라 최종 결과를 클라이언트에 다시 돌려주기도 한다.

템플릿/콜백의 응용

  • 템플릿에 담을 반복되는 작업 흐름은 어떤 것인지 살펴봐야 한다.
  • 템플릿이 콜백에게 전달해줄 내부의 정보는 무엇이고, 콜백이 템플릿에게 돌려줄 내용은 무엇인지도 생각해봐야 한다.
  • 템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는 것이 가장 중요하다.
    • 그에 따라 콜백 인터페이스를 정의하기 때문이다.
  • 코드의 특성이 바뀌는 경계를 잘 살피고 그것을 인터페이스를 사용해 분리한다는, 가장 기본적인 객체지향 원칙에만 충실하면 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있을 것이다.

제네릭스를 이용한 콜백 인터페이스

  • 제네릭스를 이용하면 다양한 오브젝트 타입을 지원하는 인터페이스나 메서드를 정의할 수 있다.

스프링 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();
    }
}

참고자료

멀티 스레드 테스트

redisson 설정

redisson wiki

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions