Skip to content

3장 템플릿 #3

@nsun9505

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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions