728x90

예외처리 시 하지 말아야 할 행동

모든 예외는 반드시 적절하게 복구하거나 중단시키고 개발자에게 통보되어야 하는데

예외를 잡고 어떠한 처리도 하지 않고 넘어가거나 출력만 하는 행동과

무의미하고 무책임하게 다른 곳으로 예외를 던져버리는 행동은 

무슨 일이 있어도 해서는 안되는 행동이다.

 

예외의 종류

CheckedException

말그대로 예외를 확인하고 처리해야 하는 경우를 말하며

일반적으로 예외라고 하면 이 ChekedException을 말한다.

 

이때 예외처리는 필수이기 때문에

처리해주지 않을 경우 컴파일을 에러가 발생한다.

 

대표적으로 IOException, SQLException 등이 있다.

UncheckedException

RuntimeException 클래스를 상속 받은 예외들로

ChekedException처럼 예외처리가 필수는 아니다.

 

컴파일에는 문제 없지만 런타임 시에 문제가 생기는 경우로

개발자의 부주의로 인한 문제기 때문에 예외처리가 아니더라도

코드의 수정을 통해서 방지할 수 있다.

 

대표적으로 아무것도 할당되지 않은 빈 참조변수를 사용하는 경우

발생하는 NullPointerException이 있다.

Error

Error 클래스를 상속 받는 서브 클래스들을 뜻한다.

 

시스템적으로 비정상적인 오류가 발생한 경우로

보통 자바 가상머신에서 발생시키기 때문에 개발자가 코드로 잡을 수 있는 것이 아니다.

 

대표적으로 OutOfMemoryError가 있는데

개발자가 이러한 현상을 예방하기 위한 코드를 작성하는 것은 가능해도

이러한 에러가 생긴 경우를 처리하는 코드를 작성할 수는 없다.

 

그렇기 때문에 Error는 개발자가 직접 신경쓸 영역이 아니다.

Exception

Exception 클래스를 상속 받는 서브 클래스들로 에러와는 다르게

개발자가 작성한 코드에 문제가 있는 경우 발생한다.

 

이 말은 개발자가 직접 예외를 처리할 수 있다는 것인데,

Exception 클래스의 서브 클래스 중에서

RuntimeException 클래스의 상속 여부에 따라

상속 받지 않는 CheckedException과 상속 받는 UncheckedException이 있다.

Runtime Exception

RuntimeException 클래스를 상속 받은 서브 클래스들이 해당된다.

 

예외처리 방법

예외 복구

말그대로 예외가 어떻게 발생했는지 파악하고 해결하여 정상적으로 만드는 방법이다.

 

특정 횟수만큼 반복해서 재시도해보거나 다른 방법으로 전환하여 시도하는 등의

방법을 통하여 복구 시도를 하는데 어떤 식으로든 예외를 복구할 가능성이

있는 경우에만 해당 방법을 사용한다.

예외처리 회피

예외를 직접 처리하는 것이 아닌 호출한 쪽으로 떠넘기는(던지는) 방법이다.

 

예외를 던질 때는 받는 곳에서 해당 예외를 처리할 수 있어야 하는데

예외를 처리하지도 못하는 곳에 무작정 던지고 보는 것은

무책임한 회피 방식이기에 좋지 않다.

 

객체지향에서 하나의 객체는 관련된 기능만 수행하게 설계되니

만약 DAO에서 발생하는 예외는 꼭 그 안에서 해결해야 하며,

전혀 상관 없는 서비스나 컨트롤러에 예외를 넘기는 것은 하지 말아야 한다.

 

예외를 회피할 때는 반드시 의도가 분명한 경우에만 해야한다.

예외 전환

예외처리 회피처럼 예외를 떠넘기는 것은 똑같지만

예외를 바꿔서 던져준다는 것이 다르다.

 

이 방법은 보통 두 가지 상황에서 사용한다.

try {
	// 예외가 발생할 가능성이 있는 코드	
}
catch (SQLException s) {
	// 에러 코드가 특정 에러와 같은 경우
    // 특정 에러의 예외로 바꿔서 던져줌
	if(s.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
    	// 기존예외를 담아서 전달하는 중첩예외
    	throw DuplicateUserIdException(s);
    }
    // 그렇지 않은 경우 기존 예외를 다시 던짐
    else {
    	throw s;
    }
}

첫 번째로는 예외의 의미를 분명하게 해주는 경우에 사용하는데

IOException처럼 의미의 범위가 큰 예외를 던져주면

입출력에서 어떤 예외가 발생했는지 알기가 어렵다.

 

그래서 IOException 예외보다 좀 더 상세하고 확실한 의미를

가지고 있는 예외로 바꾸어 던져주는 방법이다.

 

또한 getCause 메서드를 이용하여 기존 예외를 알 수가 있어서

기존 예외를 담아서 중첩 예외로 던지는 것이 좋다.

try {
	OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome();
    Order order = orderHome.findByPrimaryKey(Integer id);
} catch (NamingException ne) {
	throw new EJBException(ne);
} catch (SQLException se) {
	throw new EJBException(ne);
}

두 번째로는 예외를 처리하기 쉽게 포장하는 것이다.

 

중첩 예외를 만드는 것은 똑같지만

의미를 명확한 예외로 전환하는 것이 아닌

강제로 처리해야하는 체크 예외를 언체크 예외로 바꿀 때 사용한다.

 

의미 있는 예외이거나 복구 가능한 예외가 아닌 경우

다루기 쉬운 런타임 예외로 바꾸는 것이다.

 

예외처리 전략

런타임 예외의 보편화

보통 체크 예외는 일반적인 예외, 언체크 예외는 시스템 장애나 프로그램상의 오류를 처리한다.

 

체크 예외는 복구 가능성이 조금이라도 있는 예외적인 상황이기에

이 예외의 처리를 강제한다.

 

프로그램의 경우에는 잘못된 요청을 받아서 예외가 생겼다고

종료를 해버릴 수 없기에 예외를 강제로 처리해야했지만,

서버에서는 수많은 요청 중에서 예외가 생긴 요청만 중단시키면 된다.

 

그래서 서버에서는 체크 예외의 활용도가 낮기 때문에

체크 예외를 직접적으로 다루기보단 런타임 예외로 전환해서

처리하는 것이 좋을 수도 있다.

애플리케이션 예외

의도적으로 발생시키는 예외로, 반드시 조치를 취하도록 요구하는 예외다.

 

은행 프로그램에서 돈을 꺼내는 경우에 잔고 부족 같은 상황은

언체크 예외로 그냥 넘어가고 돈을 꺼내면 안되는 상황이기에

체크 예외를 만들어서 의도적으로 예외를 발생시키는 경우가 이에 해당한다.

'Back-End > Spring' 카테고리의 다른 글

[JPA] N + 1 문제와 해결 방법  (1) 2024.03.01
슬라이스 테스트  (0) 2023.07.05
템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
728x90
  1. 데이터베이스 로직 같은 예외 발생 가능성이 높거나 공유 리소스의 반환이 필요한 코드는 예외 처리 블록으로 별도로 관리해줘야 함
  2. 일정하게 반복되는 작업 흐름에서 일부만 다른 코드가 존재하는 경우 전략 패턴을 사용하는 것이 좋고, 바뀌지 않는 부분을 컨텍스트, 바뀌는 부분을 전략으로 만들고 이 두 오브젝트를 인터페이스로 유연하게 연결한다.
  3. 클라이언트 메서드를 통해 직접 전략을 정의하고 제공하여 사용하게 한다.
  4. 클라이언트 메서드 안에 익명 내부 클래스를 사용하여 전략을 구현하면 클라이언트 메서드의 정보를 사용할 수도 있고 코드를 간결하게 작성할 수 있다.
  5. 컨텍스트가 여러 클라이언트에서 사용된다면 별도의 클래스로 분리하여 공유되게 만든다.
  6. 컨텍스트는 별도의 빈으로 등록하여 의존관계를 주입 받거나 클라이언트에서 직접 생성해서 사용한다.
  7. 템플릿/콜백 패턴은 클라이언트로부터 생성된 콜백 객체를 전달 받아 일정하고 반복적인 로직을 수행하는 템플릿이 상황에 따른 로직을 수행하는 콜백 메서드에 전달할 정보를 생성하여 전달한 후에 콜백 메서드가 전달 받은 정보로 특정 로직을 수행한 후 결과를 리턴하여 템플릿이 최종 결과를 클라이언트에 리턴하는 패턴이다.
  8. 콜백의 코드에도 일정한 패턴이 반복되면 콜백을 템플릿에 넣고 재활용하는 것이 좋다.
  9. 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다. Collback<T>
  10. 스프링에서는 JDBC를 위해 JdbcTemplate 기반의 템플릿과 콜백을 제공한다.
  11. 템플릿은 한 번에 여러개의 콜백을 사용하거나, 하나의 콜백을 여러 번 호출 가능하다.
  12. 템플릿/콜백 패턴을 설계할 때는 사이에 주고 받는 정보를 중요하게 봐야 함.

'Back-End > Spring' 카테고리의 다른 글

슬라이스 테스트  (0) 2023.07.05
예외  (0) 2023.06.15
JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
728x90

스프링이 제공하는 JDBC를 이용하는 DAO에서 사용할 수 있는 템플릿과 콜백

 

private JdbcTemplate jdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);

    this.dataSource = dataSource;
}

JDBC Template을 사용하기 위해 기존의 JdbcContext를 빼고 바꿔준다.

update

기존 전략 인터페이스의 메서드를 통해 전략을 적용했던 것과 똑같이

JDBC Template이 제공하는 콜백 중 PreparedStatementCreator 인터페이스의

createPreparedStatement 메서드를 사용하면 된다.

 

PreparedStatementCreator 타입의 콜백을 받아서 사용하는

JDBC Template의 메서드는 update()다

 

기존의 코드와 비교해보겠다.

public void deleteAll() throws SQLException {
    workWithStatementStrategy(
        new StatementStrategy() {
            public PreparedStatement makePreparedStatement(Connection c)
                    throws SQLException {
                return c.prepareStatement(executeSql());
            }
        }
    );
}
public void deleteAll() throws SQLException {
    this.jdbcTemplate.update("delete from users");
}

위가 기존의 코드고 아래가 JDBC 템플릿의 콜백 메서드를 사용한 예시다.

 

눈으로만 봐도 코드가 간결해진 것을 알 수 있는데

executeSql 메서드를 사용하여 쿼리문을 전달 받던거만 제외하면

코드의 구성은 똑같다고 볼 수 있다.

public void add(final User user) throws SQLException {
    this.jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)",
                    user.getId(), user.getName(), user.getPassword());
}

add 작업도 마찬가지로 update 메서드를 사용하여 할 수 있다.

추가하고자 하는 양식에 맞게 순서대로 파라미터로 전달해주면 된다.

queryForInt

public int getCount() throws SQLException  {
    Connection c = dataSource.getConnection();

    PreparedStatement ps = c.prepareStatement("select count(*) from users");

    ResultSet rs = ps.executeQuery();
    rs.next();
    int count = rs.getInt(1);

    rs.close();
    ps.close();
    c.close();

    return count;
}

위의 메서드에서는 쿼리를 실행한 후에 결과를 ResultSet을 통해 값을 가져온다.

해당 작업에서도 사용할 수 있는 템플릿이 있다.

 

마찬가지로 PreparedStatementCreator 콜백과 ResultSetExtractor 콜백을

파라미터로 받는 query 메서드를 사용한다.

 

ResultSetExtractor 콜백은 PreparedStatement의 쿼리를 실행해 얻은

ResultSet을 전달 받는 콜백이다.

public int getCount() {
	return this.jdbcTemplate.query(new PreparedStatementCreator() {
    	public PreparedStatement createPreparedStatement(Connection c) throws SQLException {
        	return c.preparedStatement("select count(*) from users");
        }
    }, new ResultSetExtractor<Integer>() {
    	public Integer extractorData(ResultSet rs) throws SQLException {
        	rs.next();
            return rs.getInt(1);
        }
    }
}

위의 코드는 query 메서드를 적용한 코드다.

 

살펴보면 query 메서드의 파라미터로

PreparedStatementCreator 콜백과 ResultSetExtractor 콜백을 전달 받는다.

 

PreparedStatementCreator 콜백은 템플릿으로부터

커넥션을 받아 PreparedStatement(SQL문)을 돌려준다.

 

ResultSetExtractor 콜백은 템플릿으로부터

ResultSet을 받아 추출한 결과를 돌려준다.

 

즉, query(쿼리문, ResultSet에서 추출한 결과)

 

위의 query 메서드에서 사용되는 콜백 오브젝트들은 모두 재사용하기 좋은 구조를 가지고 있는데,

PreparedStatementCreator 콜백은 재사용 방법을 알아봤으니

ResultSetExtractor 콜백의 재사용 방법을 알아보겠다.

public int getCount() {
    return this.jdbcTemplate.queryForInt("select count(*) from users");
}

위의 코드처럼 JDBC 템플릿에서 제공하는 queryForInt 메서드를 사용하면

위의 긴 코드를 한 줄로 간단하게 사용할 수도 있다.

 

JDBC 템플릿은 스프링이 제공하는 클래스이지만 DI 컨테이너를 필요로 하지 않아서

사용하고자 하는 클래스에서 JdbcTemplate 오브젝트를 생성하고

필요한 DataSource를 전달해주기만 하면 된다.

queryForObject

public User get(String id) throws SQLException {
    Connection c = this.dataSource.getConnection();
    PreparedStatement ps = c
            .prepareStatement("select * from users where id = ?");
    ps.setString(1, id);

    ResultSet rs = ps.executeQuery();

    User user = null;
    if (rs.next()) {
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }

    rs.close();
    ps.close();
    c.close();

    if (user == null) throw new EmptyResultDataAccessException(1);

    return user;
}

위의 코드는 기존의 get 메서드인데 코드가 아주 길고 복잡하다.

 

PreparedStatementCreator 콜백을 사용하여 SQL문도 처리해야하고

SQL문이 바인딩이 필요한 치환자(id)를 갖고 있으며,

쿼리 결과를 User 오브젝트로 만들어야 한다.

 

getCount 메서드처럼 특정 값을 리턴하는 것이 아닌

오브젝트를 리턴해야하기 때문에 ResultSetExtractor 콜백을 사용하는 것이 아니라

RowMapper 콜백을 사용해야 한다.

 

두 개의 콜백 모두 템플릿으로부터 ResultSet을 전달 받아 필요한 정보를 추출하지만

ResultSetExtractor은 한 번만 전달 받아 알아서 결과를 리턴해주지만

RowMapper은 로우 하나를 매핑할 때마다 호출된다.

 

get 메서드는 결국 사용자 정보를 하나 가져오는 메서드이기 때문에

하나의 사용자 정보만 매핑하면 되니 ResultSet의 첫 번째 로우만 매핑한다.

 

이러한 작업들을 해주는 템플릿 메서드는 queryForObject가 있다.

queryForObject(쿼리문, 조회할 조건(바인딩할 파라미터 값), RowMapper 콜백으로 매핑)

위와 같은 양식으로 코드를 작성하면 된다.

public User get(String id) {
    return this.jdbcTemplate.queryForObject("select * from users where id = ?",
            new Object[] {id}, 
            new RowMapper<User>() {
                public User mapRow(ResultSet rs, int rowNum)
                        throws SQLException {
                    User user = new User();
                    user.setId(rs.getString("id"));
                    user.setName(rs.getString("name"));
                    user.setPassword(rs.getString("password"));
                    return user;
                }
            });
}

코드가 엄청 혁신적으로 짧아지고 그런 것은 아니지만 어느정도 간결해졌다.

 

queryForObject 템플릿 메서드의 첫 번째 파라미터로

"select * from users where id = ?" 라는 쿼리문을 전달했고

 

두 번째 파라미터로 해당 쿼리문에 바인딩 하기 위한 기본키인 id를 전달 후에

쿼리문의 결과를 RowMapper 콜백을 통해 User 오브젝트로 매핑한다.

 

query()

queryForObject 템플릿 메서드는 하나의 사용자만 가져오는 경우에 사용했다면,

모든 사용자의 정보를 가져오는 기능에 사용하는 템플릿 메서드도 있다.

 

기존에는 한 명의 사용자 정보만 가져왔으나 이번엔 모든 사용자를 가져오니

User 오브젝트 타입의 List를 사용하면 된다.

public List<User> getAll() { }

모든 사용자의 정보를 가져오는 메서드의 시그니처는 위와 같이 선언한다.

 

queryForObject와 비슷하지만 모든 값을 가져오는 메서드이니

파라미터로 바인딩할 조건을 받을 필요가 없이

쿼리문과 RowMapper 콜백만 받는다.

 

조건이 있는 경우에는 바인딩할 파라미터를 추가한다.

return this.jdbcTemplate.query("select * from users order by id",
				new RowMapper<User>() {
					public User mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						User user = new User();
						user.setId(rs.getString("id"));
						user.setName(rs.getString("name"));
						user.setPassword(rs.getString("password"));
						return user;
					}
				});

모든 사용자 정보를 id순으로 얻는 쿼리문의 수행 결과인 ResultSet의

모든 로우를 열람하면서 로우마다 RowMapper 콜백을 호출하여

User 오브젝트로 변환하여 리스트에 추가한다.

'Back-End > Spring' 카테고리의 다른 글

예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14
템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
728x90

전략 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고

그중 일부분만 자주 바꿔 사용하는 경우 적합한 구조다.

 

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

템플릿/콜백 패턴이라고 한다.

 

컨텍스트는 템플릿에 해당하고 익명 내부 클래스로 만들어지는 오브젝트를 콜백에 해당한다.

템플릿

어떤 목적을 위해 미리 만들어둔 모양이 있는 틀로 고정된 틀 안에서

특정 부분만 상황에 맞게 바꿔 사용하는 경우를 말한다.

JdbcContext처럼 공통적인 기능에서 특정 부분만 전략에 맞게 사용하는 것과 같다.

콜백

특정 로직을 담은 메서드를 실행시키기 위해 다른 오브젝트의 메서드에 전달되는 오브젝트

JdbcContext에 전달되어 실행되는 전략과 같다.

템플릿/콜백의 동작 원리

  1. 클라이언트(CRUD 메서드)가 콜백(익명 내부 클래스)을 생성
  2. 클라이언트가 템플릿(컨텍스트)에 콜백을 전달하며 호출
  3. 템플릿의 로직 시작 및 참조정보(JDBC 정보) 생성
  4. 템플릿이 콜백을 호출하며 참조정보를 전달
  5. 클라이언트 및 전달 받은 참조정보 등을 통해 로직 수행
  6. 템플릿에 로직 결과를 리턴
  7. 템플릿의 남은 로직 수행 및 마무리
  8. 클라이언트에 템플릿의 작업 결과를 리턴

콜백의 분리와 재활용

클라이언트의 메서드가 간결해지고 최소한의 로직만 갖고 있게 된다는 장점이 있지만

익명 내부 클래스를 통해 코드를 작성하는 것은 익숙한 방식이 아니기도해서

코드의 작성이나 가독성이 떨어진다는 단점이 있다.

 

기존에 했던 작업들처럼 익명 내부 클래스를 통해 오브젝트를 만드는 코드에서

반복적으로 수행되는 작업을 분리하면 이러한 문제를 줄일 수 있다.

new StatementStrategy() {			
    public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
      return c.prepareStatement("변하는 SQL문");
    }
}

위의 코드에서 SQL문을 리턴해주는 부분만 빼면 콜백 클래스의

정의와 생성은 어느 메서드에서 사용하든 일치한다는 것을 알 수 있다.

 

반복되는 부분인 콜백 클래스의 정의와 생성을 메서드로 분리하고

분리한 메서드의 파라미터로 변하는 부분인 SQL문을 전달하면 된다.

private void executeSql(final String query) throws SQLException {
	this.jdbcContext.workWithStatementStartegy(
    	new StatementStrategy() {			
            public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
				return c.prepareStatement("변하는 SQL문");
            }
        }
    );
}

이렇게 분리된 메서드를 add와 delete 등의 DAO의 메서드에서 그대로 재사용하면 된다.

public void deleteAll() throws SQLException {
	executeSql("delete from users");
}

당연히 코드가 훨씬 간결해졌다.

콜백과 템플릿 결합하기

마찬가지로 executeSql 메서드도 모든 DAO 클래스에서 사용해야하는

공통적인 기능이기 때문에 공유하는 것이 좋다.

 

해당 메서드를 공유해서 사용할 수 있게 JdbcContext 클래스로 옮겨주기만 하면된다.

public class JdbcContext {
	public void executeSql(final String query) throws SQLException {
        workWithStatementStartegy(
            new StatementStrategy() {			
                public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                    return c.prepareStatement("변하는 SQL문");
                }
            }
        );
    }
}

성격이 다른 코드이기 때문에 분리하는 것이 좋을 수도 있지만

하나의 목적을 위해 서로 연결되어 있는 응집력이 강한 코드이기 때문에

한 곳에 모아두는 것이 오히려 유리하다.

'Back-End > Spring' 카테고리의 다른 글

템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
728x90

전략 패턴의 구조에서는

클라이언트는 add, delete 같은 메서드에 해당하고

전략은 익명 내부 클래스에 해당하며

jdbcContextWithStatementStrategy 메서드를 컨텍스트라고 볼 수 있다.

 

여기서 컨텍스트는 User의 DAO에서만 사용하는 것이 아니라

다른 DAO에서도 공통적으로 사용할 수 있는 기능에 해당하기 때문에

해당 컨텍스트를 모든 DAO에서 사용할 수 있게

별도의 클래스로 독립시키는 것이 좋다.

 

클래스 분리하기

별도의 클래스를 만들어 컨텍스트 메서드를 옮기고 DI를 알맞게 설정해준다.

public class JdbcContext {
	DataSource dataSource;
	
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	public void workWithStatementStrategy(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 {
			if (ps != null) { try { ps.close(); } catch (SQLException e) {} }
			if (c != null) { try {c.close(); } catch (SQLException e) {} }
		}
	}
}

DAO 클래스에서 해당 컨텍스트 클래스를 DI 받아서 사용할 수 있게 한다.

public class UserDao {
    private JdbcContext jdbcContext;
		
	public void setJdbcContext(JdbcContext jdbcContext) {
		this.jdbcContext = new JdbcContext();
	}
    // 생략
}

빈 의존관계 변경하기

스프링의 의존관계 주입은 기본적으로 인터페이스를 사이에 두고 의존 클래스를

상황에 맞게 유연하게 바꿔서 사용할 수 있게 하는 것이 목적이지만

JdbcContext 클래스 같이 바뀔 일이 없는 클래스는 굳이 인터페이스를 구현하여

의존관계를 주입해줄 필요는 없다.

인터페이스를 사용하지 않는 DI 적용

코드에 따라 문제가 될 수도 있고 안될 수도 있지만

JdbcContext 같은 경우는 문제가 되지 않는 정상적인 DI 방식을 적용했다고 볼 수 있다.

 

여러가지 이유가 있는데 첫 번째로는 

JdbcContext는 그저 컨텍스트 메서드를 제공하는 서비스 오브젝트로서의 의미만 있고

오브젝트 자체가 빈으로 등록되어 싱글톤으로 공유되어 사용되는 것이 이상적이다.

 

두 번째는 JdbcContext가 DI를 통해 DataSource라는 빈에 의존하고 있기 때문인데

스프링의 DI를 위해서는 주입되는 쪽과 주입받는 쪽이 모두 빈으로 등록되야 하기 때문이다.

코드를 이용하는 수동 DI

DAO 클래스 내부에서 직접 DI를 적용하는 방법인데, 이 방법을 사용하면

JdbcContext를 싱글톤으로 만드는 것은 불가능하다.

 

그렇다고 계속해서 JdbcContext 오브젝트를 생성하는 것은 아니고

DAO 오브젝트마다 하나의 JdbcContext 오브젝트를 공유하게 한다.

 

하지만 위에서 살펴봤듯이 컨테이너를 통해 DI를 받으려면

양쪽 모두 스프링 빈으로 등록이 되어있어야 하는데 이 방법을 사용하면

JdbcContext는 빈으로 등록이 되었지 않기 때문에 DataSource를 주입받을 수 없다.

 

이러한 문제를 해결하기 위해서는 DAO에 JdbcContext의 DI까지 맡기면 된다.

즉, JdbcContext아 DI 받아야 할 DataSource를 DAO가 대신 DI를 받는다.

public class UserDao {
	private DataSource dataSource;
		
    public void setDataSource(DataSource dataSource) {
        this.jdbcContext = new JdbcContext();
        this.jdbcContext.setDataSource(dataSource);

        this.dataSource = dataSource;
    }

    private JdbcContext jdbcContext;
}

이렇게 JdbcContext 대신 DataSource를 DI 받는다.

'Back-End > Spring' 카테고리의 다른 글

JDBC Template  (0) 2023.06.13
템플릿과 콜백  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
스프링에 테스트 적용하기  (0) 2023.06.08
728x90

전략 패턴을 사용하여 메서드 추출을 했을 때보다 

유연하고 확장성 있는 코드를 짜는 것은 성공했지만

여전히 필요할 때마다 전략 패턴을 새로 생성해야 하는 것은 그대로다.

 

또한, 전략에 부가적인 정보가 필요한 경우

이를 위해 생성자와 인스턴스 변수를 추가적으로 만들어야 하는 것도 번거롭다.

 

로컬 클래스로 사용하기

전략 클래스를 계속해서 생성해야 하는 문제는 전략 클래스를

컨텍스트 클래스에 내부 클래스로 정의하는 것으로 해결할 수 있다.

 

이러한 전략 클래스들은 컨텍스트 클래스 이외에는 사용되지 않기 때문에

굳이 외부에 클래스로 따로 만들어둘 필요가 없기 때문이다.

public void add(User user) throws SQLException {
	// 사용할 컨텍스트 메소드 내부에 클래스 선언
	class AddStatement implements StatementStartegy {
    	// 전략 구현
    }
}

기존에 별도의 클래스에 작성되어 있던 코드를 위와 같이 해당 전략을 사용하고자 하는

컨텍스트 메서드 내부에 선언하면 클래스 파일을 추가하는 작업이 사라진다.

 

또한 해당 컨텍스트 메서드에서 사용되는 멤버들에 접근할 수 있다는 장점도 있기 때문에

생성자를 통해 필요한 오브젝트들을 일일히 생성하는 번거로움이 사라지고

컨텍스트 메서드의 파라미터나 멤버들에 접근해 사용하면 된다.

익명 내부 클래스로 더 간결하게 만들기

특정 컨텍스트 메서드에서만 사용될 전략 클래스이기 때문에

다른 곳에서 호출 할 필요가 없으니 클래스의 이름이 굳이 필요 없다.

public void add(User user) throws SQLException {
	jdbcContextWithStatementStrategy(
		StatementStrategy st = new StatementStrategy() { // 전략 구현부 }
    );
}

어차피 한 번만 사용할 오브젝트니 굳이 변수에 저장할 필요도 없다.

public void add(User user) throws SQLException {
	jdbcContextWithStatementStrategy(
		new StatementStrategy() { // 전략 구현부 }
    );
}

클래스 파일도 줄어들고 코드도 훨씬 간결해졌다.

'Back-End > Spring' 카테고리의 다른 글

템플릿과 콜백  (0) 2023.06.13
컨텍스트와 DI  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
스프링에 테스트 적용하기  (0) 2023.06.08
JUnit 자세히 알아보기  (0) 2023.06.08
728x90

개방 폐쇄 원칙

이전에 알아본 내용이지만 다시 간단하게 알아보자면

변경에 개방적이어야 하는 코드들은 수정과 확장에 열려있어야 하고

변경되지 말아야 하는 코드들은 닫혀있어야 하는 원칙이다.

템플릿

개방 폐쇄 원칙을 적용하여 효과적으로 활용할 수 있도록 하는 방법으로

바뀌는 성질이 다른 코드 중 변경이 거의 없고 일정한 패턴으로 유지되는 부분을

자유롭게 변경되는 부분으로부터 독립시키는 방법이다.

 

즉, 이전에 학습했던 객체지향적인 리팩토링처럼

변하지 않고 많은 곳에서 중복되는 코드와 자주 변경되는 코드를 분리하는 작업이다.

 

템플릿 적용하기

1. 변하는 성격이 다른 것 찾기

데이터베이스를 조작하는 코드에서 예시를 든다면

조작을 위한 SQL문과 전체적인 데이터베이스 로직을 처리하는 반복적인 코드가 있다.

 

여기서 SQL문은 상황에 따라 달라지는 자주 변경되는 코드이고

데이터베이스의 전체적인 로직을 처리하는 코드는 공통적으로 사용되는 코드로

SQL문과 관련된 코드가 변하는 성격이 다른 것이라고 볼 수 있다.

2. 코드에서 분리하기

Connection c = null;
PreparedStatement ps = null;

try {
    c = dataSource.getConnection();

    ps = c.prepareStatement("delete from users"); // 변하는 부분

    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) {} }
}

위의 코드에서 변하는 부분인 SQL문을 분리하는 방법 중에서는

여러가지 방법이 있지만 기존 리팩토링 과정에서 써봤던

메서드 추출이라는 방법이 있다.

try {
    c = dataSource.getConnection();

    ps = makeStatement(c); // 변하는 부분

    ps.executeUpdate();
}

private PreparedStatement makeStatement(Connection c) throws SQLException {
    PreparedStatement ps;
    ps = c.prepareStatement("delete from users");
    return ps
}

자주 바뀌는 SQL 부분의 코드를 메서드로 분리시켰지만

이로 인해 별다른 이득을 보는 것은 없고 그냥 코드를 분리하기만 했다.

 

실질적으로 재사용되는 코드는 SQL 코드가 아니라 

데이터베이스 작업을 처리하는 코드이기 때문이다.

3. 템플릿 메서드 패턴 적용하기

템플릿 메서드 패턴은 상속을 통해 기능을 확장해서 사용한다.

 

변하지 않는 부분은 슈퍼 클래스에, 변하는 부분은 추상 메서드로 정의하여

서브 클래스에서 자신에 맞게 오버라이드하여 정의하여 쓰게 한다.

private PreparedStatement makeStatement(Connection c) throws SQLException {
    PreparedStatement ps;
    ps = c.prepareStatement("delete from users");
    return ps;
}

위의 메서드는 변하는 부분에 속하는 코드이니 해당 메서드를 추상 메서드로 선언한다.

abstract public class UserDao {
	abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
}

이제 필요에 따라 해당 추상 클래스를 상속 받는 서브 클래스에

추상 메서드를 구현하여 자유롭게 사용할 수 있다.

public class UserDaoDeleteAll extends UserDao {
    protected PreparedStatement makeStatement(Connection c) throws SQLException {
        PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

하지만 DAO 로직마다 상속을 통해 새로운 클래스를 매번 만들어야 하기 때문에 좋은 방법이 아니다.

4. 전략 패턴 적용하기

템플릿 메서드 패턴의 장점을 가지면서도 유연하고 확장성이 뛰어난 패턴으로

 

컨텍스트(변하지 않는 부분) 오브젝트와 전략 오브젝트(변하는 부분)를 아예 둘로 분리하고

이 둘의 오브젝트를 인터페이스를 통해서만 의존하도록 하는 디자인 패턴이다.

 

즉, 컨텍스트 - 전략 인터페이스 - 전략(전략 인터페이스의 구현 클래스) 구조로 생겼다고 생각할 수 있다.

public interface StatementStrategy {
	PreparedStatement makePreparedStatement(Connection c) throws SQLException; 
}

위의 코드처럼 전략 인터페이스를 생성하고 이를 구현하는 전략 클래스를 만든다.

public class Delete implements StatementStartegy {
	PreparedStatement makePreparedStatement(Connection c) throws SQLException {
    	PreparedStatement ps = c.prepareStatement("delete from users");
        return ps;
    }
}

만들어진 전략 클래스를 기존의 컨텍스트에서 사용하면 된다.

try {
    c = dataSource.getConnection();

    StatementStartegy startegy = new Delete();
    ps = startegy.makePreparedStatement(c);

    ps.executeUpdate();
}

하지만 이는 컨텍스트에 어떤 전략을 적용할지 직접적으로 지정하여

고정된 상태이기 때문에 결코 좋은 방식이 아니다.

5. 클라이언트와 컨텍스트를 분리하기

컨텍스트가 자신이 어떤 전략을 사용할지 모르게 하고

클라이언트가 어떤 전략을 사용할지 정해주어 결합을 낮춰야 하는데

이러한 방식은 1장을 공부할 때 사용했던 DI와 같다.

StatementStartegy startegy = new Delete();

위의 코드는 컨텍스트가 아닌 클라이언트가 가지고 있어야 할 코드이기 때문에

해당 부분을 클라이언트에게 넘겨야 한다.

public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException {
	try {
        c = dataSource.getConnection();
        
        ps = startegy.makePreparedStatement(c);

        ps.executeUpdate();
    }
}

이렇게 코드를 수정하면 컨텍스트가 호출될 때 클라이언트로부터 전략을 전달 받기 때문에

핵심적인 내용만 잘 가지고 있는 코드다.

public void deleteAll() throws SQLException {
	StatementStartegy = st = new Delete();
    jdbcContextWithStatementStrategy(st);
}

이제 해당 컨텍스트를 호출하는 메서드를 클라이언트라고 볼 수 있는데

클라이언트 메서드에서는 사용할 전략을 선택하여 생성하고

이를 컨텍스트 메서드에 파라미터로 넘겨주면

컨텍스트 메서드는 전달 받은 전략에 맞는 작업을 수행한다.

 

컨텍스트와 클라이언트를 다른 클래스로 서로 분리하진 않았지만

관심사와 의존관계, 책임을 분리하는 것은 끝났다.

 

'Back-End > Spring' 카테고리의 다른 글

컨텍스트와 DI  (0) 2023.06.13
전략 패턴 최적화 하기  (0) 2023.06.13
스프링에 테스트 적용하기  (0) 2023.06.08
JUnit 자세히 알아보기  (0) 2023.06.08
테스트  (0) 2023.06.07
728x90

JUnit은 각각의 테스트독립적으로 존재할 수 있게 하기 위해

테스트 메서드마다 서로 다른 오브젝트를 만들어서 실행되게 한다.

 

하지만 이 경우가 스프링에서는 문제가 될 수 있는데

애플리케이션 컨텍스트를 테스트 오브젝트마다 반복적으로 만들기 때문에

빈이 많아지고 설정이 복잡해지는 경우에는 컨텍스트 생성에 시간이 많이 소요된다.

 

또한 여러 테스트가 함께 참조해야할 애플리케이션 컨텍스트를

오브젝트 레벨에 저장해두면 각각 다른 컨텍스트를 참조하게 되어 곤란하다.

 

이를 위해서 JUnit은 테스트 클래스 전체에서 딱 한 번만 실행되는

@BeforeClass 스태틱 메서드라는 기능을 제공한다.

 

물론 이 기능으로도 가능하지만 스프링에서 제공하는 더 좋은 방법이 있다.

스프링 테스트 컨텍스트 프레임워크

적용하기

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
	@Autowired
	ApplicationContext context;
	//생략
	@Before
	public void setUp() {
		this.dao = this.context.getBean("userDao", UserDao.class);
		//생략
	}
}

@RunWith 어노테이션JUnit 테스트 실행 방법을 확장할 때 사용하는 어노테이션으로

SpringJUnit4ClassRunner라는 확장 클래스를 지정하여 해당 클래스가

테스트 중에 사용할 애플리케이션 컨텍스트를 만들고 관리해준다.

 

@ContextConfiguration 어노테이션은 애플리케이션 컨텍스트의 설정파일의 위치를 지정한다.

 

이렇게 간단하게 하나의 테스트 클래스 내에서 하나의 테스트용 애플리케이션 컨텍스트를 공유할 수 있다.

 

또한 @ContextConfiguration 어노테이션설정파일이 같은 경우에는

같은 애플리케이션 컨텍스트로 보고 서로 다른 테스트 클래스 간이라도

하나의 애플리케이션 컨텍스트를 공유하게 된다.

 

즉, 설정 파일의 종류만큼만 애플리케이션 컨텍스트를 생성하는 것이다.

@Autowired

해당 어노테이션은 변수 타입과 일치하는 컨텍스트 내의 빈을 찾아서 일치하는 경우

별도의 DI 설정 없이 변수에 주입해주는데, 이러한 방법을 타입에 의한 자동 와이어링이라 한다.

 

하지만 위의 코드에서는 애플리케이션 컨텍스트 타입의 변수에 해당 어노테이션을 적용하여

제 3자로서 의존관계를 주입하던 애플리케이션 컨텍스트가 역으로 의존관계 주입이 되버렸다.

 

이는 애플리케이션 컨텍스트도 빈이기 때문인데

애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다.

 

애플리케이션 컨텍스트를 받을 수 있다는 것은 컨텍스트에 등록된 모든 빈들을

getBean을 사용하지 않고도 직접 의존관계 주입을 받을 수도 있다는 것이다.

 

하지만 타입과 일치하는 빈이 두 개 이상인 경우에는 사용 할 수 없다.

의존관계 주입과 테스트

효율적인 테스트를 손쉽게 만들기 위해서는 작은 단위의 대상에 대해 독립적으로 테스트를 해야 하는데

이를 위해서는 테스트에도 DI를 적용할 수 있다.

1. 테스트 코드에 의한 DI

수정자 메서드는 테스트 코드에서 얼마든지 호출하여 사용할 수 있기 때문에

이를 이용해서 테스트 코드에서 직접 의존관계를 주입할 수 있는데,

테스트 코드에서 의존관계를 변경할 수 있게 된다는 말이다.

 

설정파일을 수정하지 않고도 의존관계를 상황에 맞게 수정할 수 있지만

한 번 바꾼 의존관계는 남아있는 모든 테스트에 적용되기 때문에 잘 사용해야 한다.

 

하지만 이러한 문제를 해결하기 위해 @DirtiesContext 어노테이션을 사용하여

의존관계를 바꾸는 테스트가 수행을 마친 후에

새로운 애플리케이션 컨텍스트를 생성하여 나머지 테스트들을 수행하게 할 수 있다.

2. 테스트용 DI 설정하기

위의 방법은 매번 새로운 애플리케이션 컨텍스트를 만들어야 하기 때문에 효과적이지 못하다.

 

그래서 애초에 실제 서버에서 사용될 설정파일과 테스트용으로 사용될 설정파일을

각각 따로 만들어서 사용하는 것이 효과적이다.

3. 스프링 컨테이너 사용하지 않고 DI 테스트 하기

초반에 작성했던 코드처럼 테스트 코드에서

상황에 맞게 직접 의존관계를 주입하여 테스트 할 수 있다.

 

DI를 통해 오브젝트들의 관심사를 분리하고 의존성을 낮췄기 때문에

이렇게 애플리케이션 컨텍스트를 만드는 번거로움을 없애 테스트 시간을

절약할 수 있는 테스트 코드를 짤 수도 있다.

테스트 방법 선택하기

위의 세 가지 테스트 방법들은 각각의 장단점이 있기 때문에

모든 방법을 상황에 맞춰 유용하게 사용할 수 있다.

 

항상 우선적으로는 테스트 수행 속도가 가장 빠르고 간결한

스프링 컨테이너를 사용하지 않는 테스트 방법을 고려하는 것이 좋다.

 

하지만 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트의

테스트를 수행해야 하는 경우에는 스프링의 설정을 이용한

DI 방식의 테스트를 사용하는 것이 편리하다.

 

예외적인 의존관계를 강제로 구성해서 테스트를 해야하는 경우에는

수동으로 의존관계를 주입하는 방식을 사용하는 것이 좋다.

'Back-End > Spring' 카테고리의 다른 글

전략 패턴 최적화 하기  (0) 2023.06.13
템플릿 적용하기  (0) 2023.06.13
JUnit 자세히 알아보기  (0) 2023.06.08
테스트  (0) 2023.06.07
XML 설정  (0) 2023.06.07

+ Recent posts