토비의 스프링 4장 (예외)

예외

JdbcTemplate을 대표로 하는 스프링의 데이터 액세스 기능에 담겨 있는 예외처리와 관련된 접근 방법에 대해 알아보자.

사라진 SQLException

JdbcTemplate을 적용한 코드에서는 SQLException이 사라졌다. 이 SQLException은 어디로 간 것일까?

초난감 예외처리

예외가 발생하면 그것을 catch 블록을 써서 잡아내는 것까지는 좋은데 그리고 아무것도 하지 않고 별문제 없는 것처럼 넘어가 버리는 건 정말 위험한 일이다. 원치 않는 예외가 발생하는 것보다도 훨씬 더 나쁜 일이다. 왜냐하면 프로그램 실행 중에 어디선가 오류가 있어서 예외가 발생했는데 그것을 무시하고 계속 진행해버리기 때문이다. 결국 발생한 예외로 인해 어떤 기능이 비정상적으로 동작하거나, 메모리나 리소스가 소진되거나, 예상치 못한 다른 문제를 일으킬 것이다.

예외를 처리할 때 반드시 지켜야할 핵심 원칙은 한 가지다. 모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.

SQLException이 발생하는 이유는 SQL에 문법 에러가 있거나 DB에서 처리할 수 없을 정도로 데이터 액세스 로직에 심각한 버그가 있거나, 서버가 죽거나 네트워크가 끊기는 등의 심각한 상황이 벌어졌기 때문이다.

예외를 처리하는 2가지 나쁜 습관은 어떤 경우에도 용납하지 않아야 한다.

  • 예외 블랙홀
  • 무의미하고 무책임한 throws

예외의 종류와 특징

예외를 어떻게 다뤄야 할까? 가장 큰 이슈는 체크 예외(checked exception)라고 불리는 명시적인 처리가 필요한 예외를 사용하고 다루는 방법이다. 자바에서 throw를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.

Error

첫째는 java.lang.Error 클래스의 서브클래스들이다. 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다. 그래서 주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안된다. 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 된다.

Exception과 체크 예외

java.lang.Exception 클래스와 그 서브클래스로 정의되는 예외들은 에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다.

Exception 클래스는 다시 체크 예외(checked exception)언체크 예외(Unchecked exception)로 구분된다. 전자는 Exception 크래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것들이고, 후자는 RuntimeException을 상속한 클래스들을 말한다. RuntimeException은 Exception의 서브클래스이므로 Exception의 일종이긴 하지만 자바는 이 RuntimeException과 그 서브클래스는 특별하게 다룬다.

일반적으로 예외라고 하면 Exception 클래스의 서브클래스 중에서 RuntimeException을 상속하지 않은 것만을 말하는 체크 예외라고 생각해도 된다. 체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다. 사용할 메소드가 체크 예외를 던진다면 이를 catch 문으로 잡든지, 아니면 다시 throws를 정의해서 메소드 밖으로 던져야 한다. 그렇지 않으면 컴파일 에러가 발생한다.

RuntimeException과 언체크/런타임 예외

java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다. 또는 대표 클래스 이름을 따서 런타임 예외라고도 한다. 에러와 마찬가지로 이 런타임 예외는 catch 문으로 잡거나 throws로 선언하지 않아도 된다. 물론 명시적으로 잡거나 throw로 선언해줘도 상관없다.

대표적으로 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullPointerException이나, 허용되지 않는 값을 사용해서 메소드를 호출할 때 발생하는 IllegalArgumentException 등이 있다. 이런 예외는 코드에서 미리 조건을 체크하도록 주의 깊게 만든다면 피할 수 있다. 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생하도록 만든 것이 런타임 예외다. 따라서 런타임 예외는 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws를 사용하지 않아도 되도록 만든 것이다.

예외처리 방법

예외 복구

첫 번째 예외처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다. 예외처리 코드를 강제하는 체크 예외들은 예외를 어떤 식으로든 복구할 가능성이 있는 경우에 사용한다.

예외처리 회피

두 번째 방법은 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다. throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 것이다. 예외를 자신이 처리하지 않고 회피하는 방법이다.

JdbcTemplate이 사용하는 콜백 오브젝트는 메소드 선언을 보면 알겠지만 ResultSet이나 PreparedStatement 등을 이용해서 작업하다 발생하는 SQLException을 자신이 처리하지 않고 템플릿으로 던져버린다. 콜백 오브젝트의 메소드는 모두 throws SQLException이 붙어있다. SQLException을 처리하는 일은 콜백 오브젝트의 일이 아니라고 보기 때문이다. 콜백 오브젝트의 메소드는 SQLException에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 던져준다.

예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.

예외 전환

마지막으로 예외를 처리하는 방법은 예외 전환을 하는 것이다. 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다. 하지만 예외 회피와 달리, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다.

예외 전환은 보통 두 가지 목적으로 사용된다.

첫째는 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서다. API가 발생하는 기술적인 로우레벨을 상황에 적합한 의미를 가진 예외로 변경하는 것이다. 보통 전환하는 예외에 원래 발생한 예외를 담아서 **중첩 예외(nested exception)**로 만드는 것이 좋다. 중첩 예외는 getCause() 메소드를 이용해서 처음 발생한 예외가 무엇인지 확인할 수 있다.

두번째 전환 방법은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다. 중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식은 같다. 하지만 의미를 명확하게 하려고 다른 예외로 전환하는 것이 아니다. 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.

일반적으로 체크 예외를 계속 throws를 사용해 넘기는 건 무의미하다. 메소드 선언은 지저분해지고 아무런 장점이 없다. DAO에서 발생한 SQLException이 웹 컨트롤러 메소드까지 명시적으로 전달된다고 해서 무슨 소용이 있을까? 어차피 복구가 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해 던지게 해서 다른 계층의 메소드를 작성할 때 불필요한 throws 선언이 들어가지 않도록 해줘야 한다.

대부분 서버 환경에서는 애플리케이션 코드에서 처리하지 않고 전달된 예외들을 일괄적으로 다룰 수 있는 기능을 제공한다.

예외처리 전략

런타임 예외의 보편화

일반적으로는 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템 장애나 프로그램사으이 오류에 사용된다고 했다. 문제는 체크 예외는 복구할 가능성이 조금이라도 있는, 말 그대로 예외적인 상황이기 때문에 자바는 이를 처리하는 catch 블록이나 throws 선언을 강제하고 있다는 점이다.

독립형 애플리케이션과 달리 서버의 특정 계층에서 예외가 밸생했을 때 작업을 일시 중지하고 사용자와 바로 커뮤니케이션하면서 예외상황을 복구할 수 있는 방법이 없다. 자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다. 자칫하면 throws Exception으로 점철된 아무런 의미도 없는 메소드들을 낳을 뿐이다. 그래서 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는게 낫다.

자바 초기부터 있었던 JDK의 API와 달리 최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다. 언체크 예외라도 필요하다면 얼마든지 catch 블록으로 잡아서 복구하거나 처리할 수 있다. 하지만 대개는 복구 불가능한 상황이고 보나마나 RuntimeException 등으로 포장해서 던져야 할 테니 아예 API 차원에서 런타임 예외를 던지도록 한 것이다.

애플리케이션 예외

런타임 예외 중심의 전략은 굳이 이름을 붙이자면 낙관적인 예외처리 기법이라고 할 수 있다. 일단 복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 어차피 런타임 예외이므로 시스템 레벨에서 알아서 처리해줄 것이고, 꼭 필요한 경우는 런타임 예외라도 잡아서 복구하거나 대응해줄 수 있으니 문제 될 것이 없다는 낙관적인 태도를 기반으로 하고 있다.

보통 시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키는 애플리케이션 예외를 가지는 메소드를 설계하는 방법에는 두 가지가 있다.

첫 번째 방법은 정상적인 처리를 했을 경우와 애플리케이션 자체의 로직에 의해 의도적으로 예외를 발생시키고자 하는 경우에 각각 다른 종류의 리턴 값을 돌려주는 것이다. 하지만 이렇게 리턴 값으로 결과를 확인하고, 예외상황을 체크하면 불편한 점도 있다. 우선 예외 상황에 대한 리턴 값을 명확하게 코드화하고 잘 관리하지 않으면 혼란이 생길 수 있다. 또 한 가지 문제는 결과 값을 확인하는 조건문이 자주 등장한다는 점이다.

두 번째 방법은 정상적인 흐름을 따르는 코드는 그대로 두고, 애플리케이션 자체의 로직에 의해 의도적으로 예외를 발생시키고자 하는 경우에는 비즈니스적인 의미를 띤 예외를 던지도록 만드는 것이다. 이때 사용하는 예외는 의도적으로 체크 예외로 만든다. 그래서 개발자가 잊지 않고 자주 발생 가능한 예외상황에 대한 로직을 구현하도록 강제해주는 게 좋다.

SQLException은 어떻게 됐나?

먼저 생각해볼 사항은 SQLException은 과연 복구가 가능한 예외인가이다. 대부분의 SQLException은 복구가 불가능하다. 더군다나 DAO 밖에서 SQLException을 다룰 수 있는 가능성은 거의 없다. 따라서 예외처리 전략을 적용해야 한다. 필요도 없는 기계적인 throws 선언이 등장하도록 방치하지 말고 가능한한 빨리 언체크/런타임 예외로 전환해줘야 한다.

스프링의 JdbcTemplate은 바로 이 예외처리 전략을 따르고 있다. JdbcTemplate 템플릿과 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던져준다. 따라서 JdbcTemplate을 사용하는 UserDao 메소드에선 꼭 필요한 경우에만 런타임 예외인 DataAccessException을 잡아서 처리하면 되고 그 외의 경우에는 무시해도 된다.

그 밖에도 스프링의 API 메소드에 정의되어 있는 대부분의 예외는 런타임 예외다. 따라서 발생 가능한 예외가 있다고 하더라도 이를 처리하도록 강제하지 않는다.

예외 전환

예외를 다른 것으로 바꿔서 던지는 예외 전환의 목적은 두 가지이다. 하나는 런타임 예외로 포장해서 굳이 필요하지 않은 catch/throws를 줄여주는 것이고, 다른 하나는 로우레벨의 예외를 좀 더 의미 있고 추상화된 예외로 바꿔서 던져주는 것이다.

스프링의 JdbcTemplate이 던지는 DataAccessException은 일단 런타임 예외로 SQLException을 포장해주는 역할을 한다. 그래서 대부분 복구가 불가능한 예외인 SQLException에 대해 애플리케이션 레벨에서는 신경 쓰지 않도록 해주는 것이다. 또한 DataAccessException은 SQLException에 담긴 다루기 힘든 상세한 예외정보를 의미 있고 일관성 있는 예외로 전환해서 추상화해주려는 용돌 쓰이기도 한다.

JDBC의 한계

JDBC는 자바 표준 JDK에서도 가장 많이 사용되는 기능 중의 하나다.

호환성 없는 SQLException의 DB 에러정보

DB마다 SQL 뿐만 아니라 에러의 종류와 원인도 제각각이다. 그래서 JDBC는 데이터 처리 중에 발생하는 다양한 예외를 그냥 SQLException 하나에 모두 담아버린다. SQLException은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 부가적으로 제공한다. getSQLState() 메소드로 예외상황에 대한 상태정보를 가져올 수 있다. 이 상태정보는 DB별로 달라지는 에러 코드를 대신할 수 있도록, 스펙에 정의된 SQL 상태 코드를 따르도록 되어있다. 그러나 결국 호환성 없는 에러 코드와 표준을 잘 따르지 않는 상태 코드를 가진 SQLException 만으로 DB에 독립적인 유연한 코드를 작성하는 건 불가능에 가깝다.

DB 에러 코드 매핑을 통한 전환

SQLException에 담긴 SQL 상태 코드는 신뢰할 만한게 아니므로 더 이상 고려하지 않는다. 차라리 DB 업체별로 만들어 유지해오고 있는 DB 전용 에러 코드가 더 정확한 정보라고 불 수 있다.

스프링은 DataAccessException이라는 SQLException을 대체할 수 있는 런타임 예외를 정의하고 있을 뿐 아니라 DataAccessException의 서브클래스로 세분화된 예외 클래스들을 정의하고 있다. 디에터 엑세스 작업 중에 발생 할 수 있는 예외 상황을 수십 가지 예외로 분류하고 이를 추상화해 정의한 다양한 예외 클래스를 제공한다.

JdbcTemplate은 SQLException을 단지 런타임 예외인 DataAccessException으로 포장하는 것이 아니라 DB의 에러 코드를 DataAccessException 계층구조의 클래스 중 하나로 매핑해준다. 전환되는 JdbcTemplate에서 던지는 예외는 모두 DataAccessException의 서브클래스 타입이다. DB별로 미리 준비된 매핑정보를 참고해서 적절한 예외 클래스를 선택하기 때문이 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있다. 데이터 엑세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.

JdbcTemplate을 이용한다면 JDBC에서 발생하는 DB 관련 예외는 거의 신경 쓰지 않아도 된다.

JDK 1.6에 포함된 JDBC 4.0부터는 기존에 JDBC의 단일 예외 클래스였던 SQLException을 스프링의 DataAccessException과 비슷한 방식으로 좀 더 세분화해서 정의하고 있다.

DAO 인터페이스와 DataAccessException 계층구조

DataAccessException은 JDBC의 SQLException을 전환하는 용도로만 만들어진 건 아니다. JDBC 외의 자바 데이터 엑세스 기술에서 발생하는 예외에도 적용된다.

DataAccessException은 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다. 데이터 액세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.

DAO 인터페이스와 구현의 분리

DAO를 굳이 따로 만들어서 사용하는 이유는 무엇일까? 가장 중요한 이유는 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서다.

대부분의 데이터 액세스 예외는 애플리케이션에서는 복구 불가능하거나 할 필요가 없는 것이다. 그렇다고 모든 예외를 다 무시해야 하는 건 아니다. 중복 키 에러처럼 비즈니스 로직에서 의미 있게 처리할 수 잇는 예외도 있다. 애플리케이션에서는 사용하지 않더라도 시스템 레벨에서 데이터 액세스 예외를 의미 있게 분류할 필요도 있다. 문제는 데이터 액세스 기술이 달라지면 같은 상황에서도 다른 종류의 예외가 던져진다는 점이다.

따라서 DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처리 방법이 달라져야 한다. 결국 클라이언트가 DAO의 기술에 의존적이 될 수 밖에 없다.

데이터 액세스 예외 추상화

스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다. 스프링의 DataAccessException은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있으며 이런 기술에서만 공통적으로 나타나는 예외를 포함해서 데이터 엑세스 기술에서 발상 가능한 대부분의 예외를 계층구조로 분류해놓았다.

JdbcTemplate과 같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다. 결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException 예외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수가 있다.

DataAccessException 활용시 주의사항

스프링을 활용하면 DB 종류나 데이터 액세스 기술에 상관없이 키 값이 중복이 되는 상황에서는 동일한 예외가 발생하리라고 기대할 것이다. 하지만 안타깝게도 DuplicateKeyException은 아직까지는 JDBC를 이용하는 경우에만 발생한다. 데이터 액세스 기술을 하이버네이트나 JPA를 사용했을 때도 동일한 예외가 발생할 것으로 기대하지만 실제로 다른 예외가 던져진다. 그 이유는 SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 하이버네이트, JDO 등에서는 각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException으로 변환하는데, DB의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다.

DataAccessException이 기술에 상관없이 어느 정도 추상화된 공통 예외로 변환해주긴 하지만 근본적인 한계 때문에 완벽하다고 기대할 수는 없다. 따라서 사용에 주의를 기울여야 한다.

스프링은 SQLException을 DataAccessException으로 전환하는 다양한 방법을 제공한다. 가장 보편적이고 효과적인 방법은 DB 에러 코드를 이용하는 것이다. SQLException을 코드에서 직접 전환하고 싶다면 SQLExceptionTranslator 인터페이스를 구현한 클래스 중에서 SQLErrorCodeSQLExceptionTranslator를 사용하면 된다.

참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리

토비의 스프링 3장 (템플릿)

템플릿

개방 폐쇄 원칙 (OCP)

어떤 부분은 변경을 통해 그 기능이 다양해지고 확장하려는 성질이 있고, 어떤 부분은 고정되어 있고 변하지 않으려는 성질이 있다. 변화의 특성이 다른 부분을 구분해주고, 각각 다른 목적과 다른 이유에 의해 다른 시점에 독립적으로 변경될 수 있는 효율적인 구조를 만들어주는 것이 개방 폐쇄 원칙이다.

템플릿이란 성질이 다른 코드 중에서 변경이 거의 일어나지 않으며 일정한 패턴으로 유지되는 특성을 가진 부분을 자유롭게 변경되는 성질을 가진 부분으로부터 독립시켜서 효과적으로 활용할 수 있도록 하는 방법이다.

일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용 가능한 풀로 관리한다. DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그런데 이런 식으로 오류가 날 때마다 미처 반횐되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있다. 그래서 JDBC 코드에서는 어떤 상황에서도 가져온 리소스를 반환하도록 try/catch/finally 구문 사용을 권장하고 있다. (finally는 try 블록을 수행한 후에 예외가 발생하든 정상적으로 처리되든 상관없이 반드시 실행되는 코드를 넣을 때 사용한다.)

어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 c(Connection)와 ps(PreparedStatment)가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다.

변하는 것과 변하지 않는 것

이런 코드를 효과적으로 다룰 수 있는 방법은 없을까? 이 문제의 핵심은 변하지 않는, 그러나 많은 곳에서 중복되는 코드와 로직에 따라 자꾸 확장되고 자주 변하는 코드를 잘 분리해내는 작업이다.

분리와 재사용을 위한 디자인 패턴 적용

메소드 추출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.prepareStatement("delete from users");
return ps;
}

자주 바뀌는 부분을 메소드로 독립시켰는데 별 이득이 없어 보인다. 왜냐하면 보통 메소드 추출 리펙토링을 적용하는 경우에는 분리시킨 메소드를 다른 곳에서 재사용할 수 있어야 하는데, 이건 반대로 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드는 DAO 로직마다 새롭게 만들어서 확장돼야 하는 부분이기 때문이다. 뭔가 반대로 됐다.

템플릿 메소드 패턴의 적용

템플릿 메소드 패턴은 상속을 통해 기능을 확장해서 사용하는 부분이다. 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의해둬서 서브클래스에서 오버라이드하여 새롭게 정의해 쓰도록 하는 것이다.

1
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
1
2
3
4
5
6
7
public class UserDaoDeleteAll extends UserDao {

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

이제 UserDao 클래스의 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있고, 확장 때문에 기존의 상위 DAO 클래스에 불필요한 변화는 생기지 않도록 할 수 있으니 객체지향 설계의 핵심 원리인 개방 폐쇄 원칙(OCP)을 그럭저럭 지키는 구조를 만들어낼 수는 있는것 같다. 그렇지만 아직 문제가 있다. 가장 큰 문제는 DAO 로직마다 상속을 통해 새로운 클래스를 만들어야 한다는 점이다. 이래서는 장점보다 단점이 더 많아 보인다.

변하지 않는 코드를 가진 UserDao의 JDBC try/catch/finally 블록과 변하는 PreparedStatement를 담고 있는 서브클래스들이 이미 클래스 레벨에서 컴파일 시점에 이미 그 관계가 결정되어 있다. 따라서 그 관계에 대한 유연성이 떨어진다.

전략 패턴의 적용

개방 폐쇠 원칙(OCP)을 잘 지키는 구조이면서도 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것이, 오브젝트를 아예 둘로 분리하고 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. 전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상화된 인터페이스를 통해 위임하는 방식이다.

deleteAll()은 JDBC를 이용해 DB를 업데이트하는 작업이라는 변하지 않는 맥락(context)을 갖는다. deleteAll()의 컨텍스트를 정리해보면 다음과 같다.

  • DB 커넥션 가져오기
  • PreparedStatement를 만들어줄 외부 기능 호출하기
  • 전달받은 PreparedStatement 실행하기
  • 예외가 발생하면 이를 다시 메소드 밖으로 던지기
  • 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기

두번째 작업에서 사용하는 PreparedStatement를 만들어주는 외부 기능이 바로 전략 패턴에서 말하는 전략이라고 볼 수 있다. 전략 패턴의 구조를 따라 이 기능을 인터페이스로 만들어두고 인터페이스 이 메소드를 통해 PreparedStatement 생성 전략을 호출해주면 된다. 여기서 눈여겨볼 것은 이 PreparedStatement를 생성하는 전략을 호출할 때는 이 컨텍스트 내에서 만들어둔 DB 커넥션을 전달해야 한다는 점이다.

PreparedStatement를 만드는 전략의 인터페이스는 컨텍스트가 만들어준 Connection을 전달받아서, PreparedStatement를 만들고 만들어진 PreparedStatement 오브젝트를 돌려준다. 이 내용을 인터페이스로 정의하면 다음과 같다.

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

StatementStrategy 인터페이스를 상속해서 실제 전략 클래스를 만들고 이 전략 클래스를 이용한 전략 패턴을 적용한 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteAll() throws SQLException {
...
try {
c = dataSource.getConnection();

StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.makePreparedStatement(c);

ps.executeUpdate();
} catch (SQLException e) {
...
}
}

전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서(OCP의 폐쇄 원칙) 전략을 바꿔 쓸 수 있다(OCP의 개방 원칙)는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다. 컨텍스트가 StatementStrategy 인터페이스 뿐 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있다는건, 전략 패턴에도 OCP에도 잘 들어맞는다고 볼 수 없기 때문이다.

DI 적용을 위한 클라이언트/컨텍스트 분리

전략 패턴에 따르면 Context가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는게 일반적이다. Client가 구체적인 전략의 하나를 선택하고 오브젝트로 만들어서 Context에 전달하는 것이다.

결국 이 구조에서 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴봤던 의존관계 주입(DI)이었다. 결국 DI란 이러한 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조라고 볼 수 있다.

아무튼 여기서 이 패턴 구조를 코드에 적용해보자. 중요한 것은 컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시켜야 한다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 {
if (ps != null) { try { ps.close(); } catch (SQLException e) {} }
if (c != null) { try { c.close(); } catch (SQLException e) {} }
}
}

이 메소드는 컨텍스트의 핵심적인 내용을 잘 담고 있다. 클라이언트로부터 StatementStrategy 타입의 전략 오브젝트를 제공받고 JDBC try/catch/finally 구조로 만들어진 컨텍스트 내에서 작업을 수행한다.

다음은 클라이언트에 해당하는 부분이다. 컨텍스트를 별도의 메소드로 분리했으니 deleteAll() 메소드가 클라이언트가 된다. deleteAll()은 전략 오브젝트를 만들고 컨텍스트를 호출하는 책임을 지고 있다.

1
2
3
4
public void deleteAll() throws SQLException {
StatementStrategy st = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); // 컨텍스트 호출. 전략 오브젝트 전달
}

클라이언트가 컨텍스트가 사용할 전략을 정해서 전달하는 면에서 DI 구조라고 이해할 수도 있다.

마이크로 DI

의존관계 주입(DI)은 다양한 형태로 적용할 수 있다. DI의 가장 중요한 개념은 제3자의 도움을 통해 두 오브젝트 사이의 유연한 관계가 설정되도록 만든다는 것이다. 이 개념만 따른다면 DI를 이루는 오브젝트와 구성요소의 구조나 관계는 다양하게 만들 수 있다.

일반적으로 DI는 의존관계에 있는 두 개의 오브젝트와 이 관계를 다이내믹하게 설정해주는 오브젝트 팩토리(DI 컨테이너), 그리고 이를 사용하는 클라이언트라는 4개의 오브직트 사이에서 일어난다. 하지만 때로는 원시적인 전략패턴 구조를 따라 클라이언트가 오브젝트 팩토리의 책임을 함께 지고 있을 수도 있다.

이런 경우에는 DI가 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다. 이렇게 DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI라고도 한다. 또는 코드에 의한 의미로 수동 DI라고 부를 수도 있다.

JDBC 전략 패턴의 최적화

전략과 클라이언트의 동거

현재 구조에 두 가지 불만이 있다.

  • DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 것.
  • DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 위해 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야 한다는 것.

이 두가지 문제를 해결할 수 있는 방법을 생각해보자.

로컬 클래스

클래스 파일이 많아지는 문제는 간단한 해결 방법이 있다. StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리는 것이다. DeleteAllStatement나 AddStatement는 UserDao 밖에서는 사용되지 않는다. 둘 다 UserDao에서만 사용되고, UserDao의 메소드 로직에 강하게 결합되어 있다.

중첩 클래스의 종류

다른 클래스 내부에 정의되는 클래스를 중첩 클래스(nested class)라고 한다. 중첩 클래스는 독립적으로 오브젝트로 만들어질 수 있는 스태틱 클래스(static class)와 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 내부 클래스(inner class)로 구분된다.

내부 클래스는 다시 범위(scope)에 따라 세가지로 구분된다.

  • 멤버 내부 클래스 : 멤버 필드처럼 오브젝트 레벨에 정의된다.
  • 로컬 클래스 : 메소드 레벨에 정의된다.
  • 익명 내부 클래스 : 이름을 갖지 않는 익명 클래스이다. 익명 내부 클래스의 범위는 선언된 위치에 따라서 다르다.

로컬 클래스의 장점은 클래스가 내부 클래스이기 때문에 자신이 선언된 곳의 정보에 접근할 수 있다는 것이다. 내부 메소드는 자신이 정의된 메소드의 로컬 변수에 직접 접근할 수 있기 때문이다. 다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언해줘야 한다.

로컬 클래스로 만들어두니, 메소드마다 추가해야 했던 클래스 파일을 하나 줄일 수 있고 내부 클래스의 특징을 이용해 로컬 변수를 바로 가져다 사용할 수 있다는 장점도 생겼다.

익명 내부 클래스

익명 내부 클래스

익명 내부 클래스(anonymous inner class)는 이름을 갖지 않는 클래스다. 클래스 선언과 오브젝트 생성이 결합된 상태로 만들어지며, 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 다음과 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없고, 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.

new 인터페이스이름() { 클래스 본문 };

익명 내부 클래스는 선언과 동시에 오브젝트를 생성한다. 이름이 없기 때문에 클래스 자신의 타입을 가질 수 없고, 구현한 인터페이스 타입의 변수에만 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
StatementStrategy st = new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}

만들어진 익명 내부 클래스의 오브젝트는 딱 한 번만 사용할 테니 굳이 변수에 담아두지 말고 jdbcContextWithStatementStrategy() 메소드의 파라미터에서 바로 생성하는 편이 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void add(final User user) throws SQLException {
jdbcContextWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?,?,?)");
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());

return ps;
}
}
);
}

컨텍스트와 DI

전략 패턴의 구조로 보자면 UserDao의 메소드가 클라이언트이고, 익명 내부 클래스로 만들어지는 것이 개별적인 전략이고, jdbcContextWithStatementStrategy() 메소드는 컨텍스트다. 그런데 JDBC의 일반적인 작업 흐름을 담고 있는 jdbcContextWithStatementStrategy()는 다른 DAO에서도 사용 가능하다. 그러니 jdbcContextWithStatementStrategy()를 UserDao 클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해야한다.

클래스 분리

분리해서 만들 클래스의 이름을 JdbcContext라고 하자. JdbcContext에 UserDao에 있던 컨텍스트 메소드를 workWithStatementStrategy()라는 이름으로 옮겨놓는다. 그런데, 이렇게 하면 DataSource가 필요한 것은 UserDao가 아니라 JdbcContext가 돼버린다. DB 커넥션을 필요로 하는 코드는 JdbcContext 안에 있기 때문이다. 따라서 JdbcContext가 DataSource에 의존하고 있으므로 DataSource 타입 빈을 DI 받을 수 있게 해줘야 한다.

빈 의존관계 변경

UserDao는 이제 JdbcContext에 의존한다. 그런데 JdbcContext는 인터페이스인 DataSource와는 달리 구체 클래스다. 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는게 목적이다. 하지만 이 경우 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성은 없다. 따라서 인터페이스를 구현하지 않고, UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.

스프링 빈으로 DI

인터페이스를 사용해서 클래스를 자유롭게 변경할 수 있게 하지는 않았지만, JdbcContext를 UserDao와 DI 구조로 만들어야 할 이유는 다음과 같다.

  1. JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이 되기 때문이다.
  2. JdbcContext가 DI를 통해 다른 빈에 의존하고 있기 때문이다. JdbcContext는 dataSource 프로퍼티를 통해 JdbcContext 오브젝트를 주입받도록 되어 있다. DI를 위해서는 주입되는 오브젝트와 주입받는 오브젝트 양쪽 모두 스프링 빈으로 등록돼야 한다. 스프링이 생성하고 관리하는 IoC 대상이어야 DI에 참여할 수 있기 때문이다.

템플릿과 콜백

전략 패턴은 복잡하지만 바뀌지 않는 일정한 패턴을 갖는 작업 흐름이 존재하고 그중 일부분만 자주 바꿔서 사용해야 하는 경우에 적합한 구조다. 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식이다. 이런 방식을 스프링에서는 템플릿/콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.

템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼클래스에 두고, 바뀌는 부분을 서브클래스의 메소드에 두는 구조로 이뤄진다.

콜백

콜백은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 자바에서는 메소드 자체를 파라미터로 전달할 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 펑서녈 오브젝트(functional object)라고도 한다.

템플릿/콜백의 동작원리

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

템플릿/콜백의 특징

여러 개의 메소드를 가진 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 달리 템플릿/콜백 패턴의 콜백은 보통 단일 메소드 인터페이스를 사용한다. 템플릿의 작업 흐름 중 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문이다. 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다고 보면된다.

템플릿/콜백 패턴의 일반적인 작업 흐름은 다음과 같다.

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

템플릿/콜백 방식은 전략 패턴과 DI의 장점을 익명 내부 클래스 사용 전략과 결합한 독특한 활용법이라고 이해할 수 있다. 단순히 전략 패턴으로만 보기엔 독특한 특징이 많으므로 템플릿/콜백을 하나의 고유한 패턴으로 기억해두면 좋다.

편리한 콜백의 재활용

템플릿/콜백 방식에서 한 가지 아쉬운 점이 있다. DAO 메소드에서 매번 익명 내부 클래스를 사용하기 때문에 상대적으로 코드를 작성하고 읽기가 조금 불편하다는 점이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteAll() throws SQLException {
executeSql("delete from users"); // 변하는 SQL 문장
}
------------------------------------------------------------
private void executeSql(final String query) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() {
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement(query);
}
}
)
}

바뀌지 않는 모든 부분을 빼내서 executeSql() 메소드로 만들었다. 바뀌는 부분인 SQL 문장만 파라미터로 받아서 사용하게 만들었다. SQL을 담은 파라미터를 final로 선언해서 익명 내부 클래스인 콜백 안에서 직접 사용할 수 있게 하는 것만 주의하면 된다.

이렇게 재사용 가능한 콜백을 담고 있는 메소드라면 DAO가 공유할 수 있는 템플릿 클래스 안으로 옮겨도 된다. 그 결과 결국 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조가 됐다.

템플릿/콜백의 응용

고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각해보는 습관을 기르자. 중복된 코드는 먼저 메소드로 분리하는 간단한 시도를 해본다. 그중 일부 작업을 필요에 따라 바꾸어 사용해야 한다면 인터페이스를 사이에 두고 분리해서 전략패턴을 적용하고 DI로 의존관계를 관리하도록 만든다. 그런데 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러 종류가 만들어질 수 있다면 이번엔 템플릿/콜백 패턴을 적용하는 것을 고려해볼 수 있다.

가장 전형적인 템플릿/콜백 패턴의 후보는 try/catch/finally 블록을 사용하는 코드다.

템플릿/콜백을 적용할 때는 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 가장 중요하다. 그에 따라 콜백의 인터페이스를 정의해야 하기 때문이다.

클래스 이름이 Template으로 끝나거나 인터페이스 이름이 Callback으로 끝난다면 템플릿/콜백이 적용된 것이라고 보면 된다.

정리

  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
  • 일정한 작업 흐름이 반복되면서 그중 일부 기능만 바뀌는 코드가 존재한다면 전략 패턴을 적용한다. 바뀌지 않는 부분을 컨텍스트로, 바뀌는 부분은 전략으로 만들고 인터페이스를 통해 유연하게 전략을 변경할 수 있도록 구성한다.
  • 클라이언트 메소드 안에 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 코드도 간결해지고 메소드의 정보를 직접 사용할 수 있어서 편리하다.
  • 컨텍스트가 하나 이상의 클라이언트 오브젝트에서 사용된다면 클래스를 분리해서 공유하도록 만든다.
  • 단일 전략 메소드를 갖는 전략 패턴이면서 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식을 템플릿/콜백 패턴이라고 한다.
  • 콜백의 코드에도 일정한 패천이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제네릭스를 이용한다.
  • 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러 번 호출할 수도 있다.
  • 템플릿/콜백을 설계할 때는 템플릿과 콜백 사이에 주고받는 정보에 관심을 둬야한다.

참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리

토비의 스프링 2장 (테스트)

테스트

스프링이 개발자에게 제공하는 가장 중요한 가치는 객체지향과 테스트이다.

테스트란 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 디버깅을 거치게 되고, 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.

보통 웹 프로그램에서 사용하는 DAO를 테스트 하는 방법은 다음과 같다. DAO를 만든 뒤 바로 테스트하지 않고, 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 코드로 다 만든다. 이렇게 만들어진 테스트 용 웹 애플리케이션을 서버에 배치한 뒤, 웹 화면을 띄워 폼을 열고, 값을 입력한 뒤 버튼을 눌러 등록해본다.

이렇게 웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔하게 쓰이는 방법이지만 단점이 너무 많다. 테스트를 하는 중에 에러가 나거나 테스트가 실패했다면, 과연 어디에서 문제가 발생했는지를 찾아내야 하는 수고도 필요하다. 하나의 테스트를 수행하는 데 참여하는 클래스와 코드가 너무 많기 때문이다.

테스트를 하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다. 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다. 관심사의 분리라는 원리가 여기에도 적용된다. 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야한다.

작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(Unit test)라고 한다. 여기서 말하는 단위란 그 크기와 범위가 어느 정도인지 딱 정해진 건 아니다. 충분히 하나의 관심에 집중해서 효율적으로 텧스트할 만한 범위의 단위라고 보면 된다.

일반적으로 단위는 작을수록 좋다. 단위를 넘어서는 다른 코드들은 신경 쓰지 않고, 참여하지도 않고 테스트가 동작할 수 있으면 좋다. 그런 차원에서 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.

단위 테스트를 하는 이유는 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서다. 이때 확인의 대상과 조건이 간단하고 명확할수록 좋다.

테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다. 그렇게 되면 자주 반복할 수 있다는 장점을 얻을 수 있다. 테스트 자체가 사람의 수작업을 거치는 방법을 사용하기 보다는 코드로 만들어져서 자동으로 수행될 수 있어야 한다는 건 매우 중요하다. 그런데 애플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보다는 별도로 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫다.

테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.

테스트 검증의 자동화

모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다. 또 테스트의 실패는 테스트가 진행되는 동안에 에러가 발생해서 실패하는 경우와, 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분해볼 수 있다. 여기서 전자를 테스트 에러, 후자를 테스트 실패로 구분할 수 있다.

테스트 중에 에러가 발생하는 것은 쉽게 확인이 가능하다. 콘솔에 에러 메시지와 긴호출 스택 정보가 출력되기 때문이다. 하지만 테스트가 실패하는 것은 별도의 확인 작업과 그 결과가 있어야만 알 수 있다.

자바에는 단순하면서도 실용적인 테스트를 위한 도구가 여러 가지 존재한다. JUnit은 이름 그대로 자바로 단위 테스트를 만들 때 유용하게 쓸 수 있다.

JUnit은 프레임워크다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다. 개발자가 만든 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행된다. 따라서 프레임워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.

main() 메소드 테스트는 그런면에서 프레임워크에 적용하기엔 적합하지 않다. 테스트가 main() 메소드로 만들어졌다는 건 제어권을 직접 갖는다는 의미이기 때문이다. 그래서 가장 먼저 할 일은 main() 메소드에 있던 테스트 코드를 일반 메소드로 옮기는 것이다. 새로 만들 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 두가지를 따라야 한다. 첫째는 메소드가 public으로 선언돼야 하는 것이고, 다른 하나는 메소드에 @Test라는 애노테이션을 붙여주는 것이다.

JUnit은 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다. @Test가 붙어있고 public 접근자가 있으며 리턴 값이 void 형이고 파라미터가 없다는 조건을 지키기만 하면된다.

1
2
3
4
5
6
7
8
public class Test {
@Test
public void add() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

...
}
}

검증 코드 변환

1
if (!user.getName().equals(user2.getName())) { ... }

이 if 문장의 기능을 JUnit이 제공해주는 assertThat이라는 스태틱 메소드를 이용해 다음과 같이 변경할 수 있다.

1
assertThat(user2.getName(), is(user.getName()));

assertThat() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매처(matcher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어 준다. is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.

JUni은 예외가 발생하거나 assertThat()에서 실패하지 않고 테스트 메소드의 실행이 완료되면 테스트가 성공했다고 인식한다.

JUnit 테스트 실행

스프링 컨테이너와 마찬가지로 JUnit 프레임워크도 자바 코드로 만들어진 프로그램이므로 어디선가 한 번은 JUnit 프레임워크를 시작시켜 줘야 한다.

어디에든 main() 메소드를 하나 추가하고, 그 안에 JUnitCore 클래스의 main 메소드를 호출해주는 간단한 코드를 넣어주면 된다. 메소드 파라미터에는 @Test 테스트 메소드를 가진 클래스의 이름을 넣어준다.

1
2
3
4
5
import org.junit.runner.jUnitCore;
...
public static void main(String[] args) {
JUnitCore.main("Springbook.user.dao.UserDaoTest");
}

JUnit은 assertThat()을 이용해 검증을 했을 때 기대한 결과가 아니면 이 **AssertionError**를 던진다. 또한 테스트 수행 중에 일반 예외가 발생한 경우에도 마찬가지로 테스트 수행은 중단되고 테스트는 실패한다.

JUnit

JUnit은 사실상 자바의 표준 테스팅 프레임워크라고 불릴만큼 폭넓게 사용되고 있다. 스프링의 핵심 기능 중 하나인 스프링 테스트 모듈도 JUnit을 이용한다. 또, 테스트 작성시 자주 필요한 편리한 여러 가지 부가기능도 제공한다. 대부분의 자바 IDE는 JUnit 테스트를 손쉽게 실행할 수 있는 JUnit 테스트 지원 기능을 내장하고 있어서 더욱 편리하게 JUnit 테스트를 만들고 활용할 수 있게 해준다.

JUnit 테스트 실행 방법

JUnitCore를 이용해 테스트를 실행하고 콘솔에 출력된 메시지를 보고 결과를 확인하는 방법은 가장 간단하긴 하지만 테스트의 수가 많아지면 관리하기가 힘들어진다는 단점이 있다. 가장좋은 JUnit 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것이다. IDE를 사용하면 JUnitCore를 이용할 때처럼 main() 메소드를 만들지 않아도 된다.

JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수도 있다. 패키지 아래에 있는 모든 JUnit 테스트를 한 번에 실행할 수도 있고, 소스 폴더나 프로젝트 전체를 선택해서 모든 테스트를 한 번에 실행할 수도 있다. 이런 면에서 JUnitCore를 사용해 테스트를 실행하는 것보다 훨씬 편리하다.

주의해야 할 점은 여러개의 테스트가 어떤 순서로 실행될지는 알 수 없다. JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다.

빌드툴

프로젝트의 빌드를 위해 ANT메이븐(Maven)같은 빌드 툴과 스크립트를 사용하고 있다면, 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행할 수 있다.

여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때도 있다. 이런 경우에는 서버에서 모든 코드를 가져와 통합하고 빌드한 뒤에 테스트를 수행하는 것이 좋다. 이때는 빌드 스크립트를 이용해 JUnit 테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된다.

포괄적인 테스트

예외조건에 대한 테스트

일반적으로는 테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다. assertThat()을 통한 검증 실패는 아니고 테스트 에러라고 볼 수 있다. 그런데 이번에는 반대로 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이고, 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다. 문제는 예외 발생 여부는 메소드를 실행해서 리턴 값을 비교하는 방법으로 확인할 수 없다는 점이다.

그런데 바로 이런 경우를 위해 JUnit은 예외조건 테스트를 위한 특별한 방법을 제공해준다. @Test 애노테이션의 expected 엘리먼트다. expected는 메소드 실행 중에 발생하리라 기대하는 예외 클래스를 넣어주면 된다.

@Test에 expected를 추가해놓으면 보통의 테스트와는 반대로, 정상적으로 테스트 메소드를 마치면 테스트가 실패하고, expected에서 지정한 예외가 던져지면 테스트가 성공한다. 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 쓸 수 있다.

테스트가 이끄는 개발

테스트 주도 개발

만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법이 있다. 이를 테스트 주도 개발(TDD)이라고 한다. 또는 테스트를 코드보다 먼저 작성한다고 해서 테스트 우선 개발(Test First Development)이라고도 한다.

“실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다”는 것이 TDD의 기본 원칙이다. TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다. 또한 TDD를 하면 자연스럽게 단위 테스트를 만들 수 있다.

TDD의 장점 중 하나는 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다는 점이다. 개발한 코드의 오류는 빨리 발견할수록 좋다. 빨리 발견된 오류는 쉽게 대응이 가능하기 때문이다. 테스트 없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류가 발생했을 때 원인을 찾기가 쉽지 않다.

테스트 코드 개선

JUnit 프레임워크는 테스트 메소드를 실행할 때 부가적으로 해주는 작업이 몇 가지 있다. 그 중에서 테스트를 실행할 때마다 반복되는 준비 작업을 별도의 메소드에 넣게 해주고, 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜주는 기능이다. 이를 알기위해서는 JUnit 프레임워크가 테스트 메소드를 실행하는 과정을 알아야 한다.

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다.
  3. @Before가 붙은 메소드가 있으면 실행한다.
  4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
  5. @After가 붙은 메소드가 있으면 실행한다.
  6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

JUnit은 @Test가 붙은 메소드를 실행하기 전과 후에 각각 @Before와 @After가 붙은 메소드를 자동으로 실행한다. 보통 하나의 테스트 클래스 안에 있는 테스트 메소드들은 공통적인 준비작업과 정리 작업이 필요한 경우가 많다. 이런 작업들을 @Before, @After가 붙은 메소드에 넣어두면 JUnit이 자동으로 메소드를 실행해주니 매우 편리하다.

대신 @Before나 @After 메소드를 테스트 메소드에서 직접 호출하지 않기 때문에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다.

또 한가지 기억해야 할 사항은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 것이다. 한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다. 그렇기 때문에 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장할 수 있다. 덕분에 인스턴스 변수도 부담 없이 사용할 수 있다. 어차피 다음 테스트 메소드가 실행될 때는 새로운 오브젝트가 만들어져서 다 초기화될 것이다.

픽스처

테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처(fixture)라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해두면 편리하다.

스프링 테스트 적용

빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성이 적지 않은 시간이 걸린다. 애플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화한다. 또 한가지 문제는 애플리케이션 컨텍스트가 초기화될 때 어떤 빈은 독자적으로 많은 리스소를 할당하거나 독립적인 스레드를 띄우기도 한다는 것이다. 이런 경우에는 테스트를 마칠 때마다 애플리케이션 컨텍스트 내의 빈이 할당한 리소스 등을 깔끔하게 정리해주지 않으면 다음 테스트에서 새로운 애플리케이션 컨텍스트가 만들어지면서 문제를 일으킬 수도 있다.

다행히도 애플리케이션 컨텍스트는 초기화되고 나면 내부의 상태가 바뀌는 일은 거의 없다. 빈은 싱글톤으로 만들었기 때문에 상태를 갖지 않는다. 따라서 애플리케이션 컨텍스트는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다. 스프링이 직접 제공하는 애플리케이션 컨텍스트 지원 기능을 사용하면 애플리케이션을 한 번만 만들어 공유해 사용할 수 있다.

테스트를 위한 애플리케이션 컨텍스트 관리

스프링은 JUnit을 이용하는 테스트 컨택스트 프레임워크를 제공한다. 테스트 컨텍스트의 지원을 받으면 간단한 애노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

먼저 ApplicationContext 타입의 인스턴스 변수를 선언하고 스프링이 제공하는 @Autowired 애노테이션을 붙인다. 마지막으로 클래스 레벨에 @RunWith@ContextConfiguration 애노테이션을 추가해준다.

1
2
3
4
5
6
7
8
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(location="/applicationContext.xml")
public class Test {
@Autowired
private ApplicationContext context;

...
}

@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.

@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다.

context 변수에는 어떻게 애플리케이션 컨텍스트가 들어가 있을까? 스프링의 JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다. 일종의 DI라고 볼 수 있는데, 애플리케이션 오브젝트 사이의 관계를 관리하기 위한 DI와는 조금 성격이 다르다.

이렇게 해서 하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있다.

테스트 클래스의 컨텍스트 공유

스프링 테스트 컨텍스트 프레임워크의 기능은 하나의 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유해주는 것이 전부가 아니다. 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.

테스트 클래스마다 다른 설정파일을 사용하도록 만들어도 되고, 몇 개의 테스트에서만 다른 설정파일을 사용할 수도 있다. 스프링은 설정파일의 종류만큼 애플리케이션 컨텍스트를 만들고, 같은 설정파일을 지정한 테스트에서는 이를 공유하게 해준다.

@Autowired

@Autowired는 스프링의 DI에 사용되는 특별한 애노테이션이다. @Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 일반적으로는 주입을 위해서는 생성자나 수정자 메소드 같은 메소드가 필요하지만, 이 경우에는 메소드가 없어도 주입이 가능하다. 또 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있는데, 이런 방법을 타입에 의한 자동와이어링이라고 한다.

스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 것이고 DI도 가능하다.

@Autowired를 이용해 애플리케이션 컨텍스트가 갖고 있는 빈을 DI 받을 수 있다면 굳이 컨텍스트를 가져와 getBean()을 사용하는 것이 아니라, 아예 빈을 직접 DI 받을 수도 있다. (@Autowired를 지정하기만 하면 어떤 빈이든 다 가져올 수 있다.)

@Autowired는 변수에 할당 가능한 타입을 가진 빈을 자동으로 찾는다. 단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없다. 타입으로 가져올 빈 하나를 선택할 수 없는 경우에는 변수의 이름과 같은 이름의 빈이 있는지 확인한다. 변수 이름으로도 빈을 찾을 수 없는 경우에는 예외가 발생한다.

테스트는 필요하다면 얼마든지 애플리케이션 클래스와 밀접한 관계를 맺고 있어도 상관없다. 개발자가 만드는 테스트는 코드 내부구조와 설정 등을 알고 있고 의도적으로 그 내용을 검증해야 할 필요가 있기 때문이다. 하지만 꼭 필요하지 않다면 테스트에서도 가능한 한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해두는 편이 좋다.

DI와 테스트

인터페이스를 통해 DI를 적용해야 하는 이유는 다음과 같다.

  • 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
  • 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
  • 테스트 때문이다.

SingleConnectionDataSource

스프링이 제공하는 가장 빠른 DataSource이다. DB 커넥션을 하나만 만들어두고 계속 사용하기 때문에 매우 빠르다. 다중 사용자 환경에서는 사용할 수 없겠지만 순차적으로 진행되는 테스트에서라면 문제없다.

스프링 테스트 컨텍스트 프레임워크를 적용했다면 애플리케이션 컨텍스트는 테스트 중에 딱 한 개만 만들어지고 모든 테스트에서 공유해서 사용한다. 따라서 애플리케이션 컨텍스트의 구성이나 상태를 테스트 내에서 변경하지 않는 것이 원칙이다. 만약 한 번 변경하면 나머지 모든 테스트를 수행하는 동안 변경된 애플리케이션 컨텍스트가 계속 사용될 것이다. 이는 별로 바람직하지 못하다.

그럴때는 @DirtiesContext라는 애노테이션을 추가한다. 이 애노테이션은 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다. 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게하기 위해서다.

@DirtiesContext는 클래스에만 적용할 수 있는 건 아니다. 하나의 메소드에서만 컨텍스트 상태를 변경한다면 메소드 레벨에 @DirtiesContext를 붙여주는 편이 낫다. 해당 메소드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.

테스트를 위한 별도의 DI 설정

테스트 코드에서 빈 오브젝트에 수동으로 DI 하는 방법은 장점보다 단점이 많다. 코드가 많아져 번거롭기도 하고 애플리케이션 컨텍스트도 매번 새로 만들어야 하는 부담이 있다.

그래서 테스트 전용 설정파일을 따로 만들어 사용하는 방법을 이용한다. 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주면 된다.

@ContextConfiguration 애노테이션에 있는 locations 엘리먼트의 값을 새로 만든 테스트용 설정파일로 변경해준다.

1
2
3
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml")
public class Test {

참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리

토비의 스프링 1장 (오브젝트와 의존관계)

스프링

스프링은 자바 엔터프라이즈 애플리케이션 개발에 사용되는 프레임워크다. 애플리케이션 프레임워크는 애플리케이션 개발을 빠르고 효율적으로 할 수 있도록 애플리케이션의 바탕이 되는 틀과 공통 프로 그래밍 모델, 기술 API 등을 제공해준다.

스프링은 스프링 컨테이너 또는 애플리케이션 컨텍스트라고 불리는 스프링 런타임 엔진을 제공한다. 스프링 컨테이너는 설정정보를 참고로 해서 애플리케이션을 구성하는 오브젝트를 생성하고 관리한다.

스프링은 세 가지 핵심 프로그래밍 모델을 지원한다.

  • IOC/DI : 오브젝트의 생명주기와 의존관계에 대한 프로그래밍 모델
  • 서비스 추상화 : 환경이나 서버, 특정 기술에 종속되지 않고 이식성이 뛰어나며 유연한 애플리케이션을 만들 수 있다.
  • AOP : 애플리케이션 코드에 산재해서 나타나는 부가적인 기능을 독립적으로 모듈화하는 프로그래밍 모델

자바빈

다음과 같은 두 가지 관례를 따라 만들어진 오브젝트를 말한다. 간단히 빈이라고 부르기도 한다.

  • 디폴트 생성자 : 자바빈은 파라미터가 없는 디폴트 생성자를 갖고 있어야 한다. 툴이나 프레임워크에서 리플렉션을 이용해 오브젝트를 생성하기 때문에 필요하다.
  • 프로퍼티 : 자자빈이 노출하는 이름을 가진 속성을 프로퍼티라고 한다. 프로퍼티는 setter와 getter를 이용해 수정 또는 조회할 수 있다.

디자인 패턴 : 소프트웨어 설계 시 특정 상황에서 자주 만나는 문제를 해결하기 위해 사용할 수 있는 재사용 가능한 솔루션. 주로 객체지향 설계에 관한 것이고, 대부분 객체지향적 설계 원칙을 이용해 문제를 해결한다.

  • 템플릿 메소드 패턴
    상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법이다. 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에 만들도록 한다. 슈퍼클래스에서는 미리 추상메소드 또는 오버라이드 가능한 메소드를 정의해두고 이를 활용해 코드의 기본 알고리즘을 담고 있는 템플릿 메소드를 만든다.
  • 팩토리 메소드 패턴
    슈퍼클래스 코드에서는 서브클래스에서 구현할 메소드를 호출해서 필요한 타입의 오브젝트를 가져와 사용한다. 이 메소드는 주로 인터페이스 타입으로 오브젝트를 리턴하므로 서브클래스에서 정확히 어떤 클래스의 오브젝트를 만들어 리턴할지는 슈퍼클래스에서는 알지 못한다. 팩토리 메소드와 메소드 패턴의 팩토리 메소드는 의미가 다르므로 혼동하지 않도록 주의해야 한다.

클래스 사이의 관계와 오브젝트 사이의 관계를 구분할 수 있어야 한다.

  • 클래스 사이의 관계 : 코드에 다른 클래스의 이름이 나타나기 때문에 만들어지는 것이다.
  • 오브젝트 사이의 관계 : 코드에서는 특정 클래스를 전혀 알지 못하더라도 해당 클래스가 구현한 인터페이스를 사용했다면, 그 클래스의 오브젝트를 인터페이스 타입으로 받아서 사용할 수 있다.

개방 폐쇄 원칙 (OCP, Open-Closed Principle)

클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다. 인터페이스를 사용해 확장 기능을 정의한 대부분의 API는 바로 이 개방 폐쇄 원칙을 따른다.

개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 고전적인 원리로도 설명이 가능하다.

전략 패턴

개방 폐쇄 원칙의 실현에도 가장 잘 들어맞는 패턴이다. 전략 패턴은 자신의 기능 맥락(context)에서, 필요에 따라 변경이 필요한 알고리즘(독립적인 책임으로 분리가 가능한 기능)을 인터페이스를 통해 통째로 외부로 분리시키고, 이를 구현한 구체적인 알고리즘 클래스를 필요에 따라 바꿔서 사용할 수 있게 하는 디자인패턴이다.

전략 패턴의 적용방법을 보면 클라이언트의 역할이 잘 설명되어 있다. 컨텍스트를 사용하는 클라이언트는 컨텍스트가 사용할 전략을 컨텍스트의 생성자 등을 통해 제공해주는게 일반적이다.

제어의 역전 (IOC, Inversion Of Control)
제어의 역전이라는 건, 간단히 프로그램의 제어 흐름 구조가 뒤바뀌는 것이다. 제어의 역전에서는 오브젝트가 자신이 사용할 오브젝트를 스스로 선택하지 않는다. 당연히 생성하지도 않는다. 또 자신도 어떻게 만들어지고 어디서 사용되는지를 알 수 없다. 모든 제어 권한을 자신이 아닌 다른 대상에게 위임하기 때문이다.

서블릿이나 JSP, EJB처럼 컨테이너 안에서 동작하는 구조는 간단한 방식이긴 하지만 제어의 역전 개념이 적용되어 있다고 볼 수 있다.

프레임워크도 제어의 역전 개념 적용된 대표적인 기술이다. 라이브러리와 프레임워크의 차이점에 대해 설명하면, 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 사용할 뿐이다. 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 보통 프레임워크 위에 개발한 클래스를 등록해주고, 프레임워크가 흐름을 주도하는 중에 개발자가 만든 애플리케이션 코드를 사용하도록 만드는 방식이다.

제어의 역전에서는 프레임워크 또는 컨테이너와 같이 애플리케이션 컴포넌트의 생성과 관계설정, 사용, 생명주기 관리 등을 관장하는 존재가 필요하다. 스프링은 IoC를 모든 기능의 기초가 되는 기반기술로 삼고 있으며, IoC를 극한까지 적용하고 있는 프레임워크다.

스프링의 IoC

오브젝트 팩토리를 이용한 스프링 IoC

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부른다. 자바빈 또는 엔터프라이즈 자바빈(EJB)에서 말하는 빈과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트를 말한다. 동시에 스프링 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말이다.

스프링에서는 빈의 생성과 관계설정 같은 제어를 담당하는 IoC 오브젝트를 빈 팩토리(bean factory)라고 부른다. 보통 빈 팩토리보다는 이를 좀 더 확장한 애플리케이션 컨텍스트(applcation context)를 주로 사용한다.

애플리케이션 컨텍스트는 그 자체로는 애플리케이션 로직을 담당하지는 않지만 IoC 방식을 이용함으로써, 별도의 정보를 참고해서 빈의 생성과 관계설정 등의 제어 작업을 총괄한다.

  • **@Configuration **: 애플리케이션 컨텍스트 또는 빈 팩토리가 사용할 설정정보라는 표시
  • @Bean : 오브젝트 생성을 담당하는 IoC용 메소드라는 표시

애플리케이션 컨텍스트는 ApplicationContext 타입의 오브젝트다. ApplicationContext를 구현한 클래스는 여러 가지가 있는데 DaoFactory처럼 @Configuration이 붙은 자바 코드를 설정정보로 사용하려면 AnnotationConfigApplicationContext를 이용하면 된다.

getBean() 메소드는 ApplicationContext가 관리하는 오브젝트를 요청하는 메소드다. getBean()은 기본적으로 Object 타입으로 리턴하게 되어 있어서 매번 리턴되는 오브젝트에 다시 캐스팅을 해줘야 하는 부담이 있다. 그러나 자바 5 이상의 제네릭 메소드 방식을 사용해 getBean()의 두 번째 파라미터에 리턴 타입을 주면, 지저분한 캐스팅 코드를 사용하지 않아도 된다.

오브젝트 팩토리에서 사용했던 IoC 원리를 그대로 적용하는데 애플리케이션 컨텍스트를 사용하는 이유는 범용적이고 유연한 방법으로 IoC 기능을 확장하기 위해서다. 애플리케이션 컨텍스트를 사용했을 때 얻을 수 있는 장점은 다음과 같다.

  • 클라이언트는 구체적인 팩토리 메서드를 알 필요가 없다.
  • 애플리케이션 컨텍스트는 종합 IoC 서비스를 제공해준다.
  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

스프링 IoC의 용어 정리


  • 빈 또는 빈 오브젝트는 스프링이 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 주의할 점은 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 다 빈은 아니라는 사실이다. 그 중에서 스프링이 직접 그 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.
  • 빈 팩토리
    스프링의 IoC를 담당하는 핵심 컨테이너를 말한다. 빈을 등록하고, 생성하고, 조회하고 돌려주고, 그 외에 부가적인 빈을 관리하는 기능을 담당한다.
  • 애플리케이션 컨텍스트
    빈 팩토리를 확장한 IoC.컨테이너다. 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 애플리케이션 컨텍스트 오브젝트는 하나의 애플리케이션에서 보통 여러 개가 만들어져 사용된다.
  • 설정정보/설정 메타정보
    스프링의 설정정보란 애플리케이션 컨텍스트 또는 빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때 사용된다.
  • 컨테이너 또는 IoC 컨테이너
    IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너 또는 IoC 컨테이너라고도 한다. 그냥 컨테이너 또는 스프링 컨테이너라고 할 때는 애플리케이션 컨텍스트를 가리키는 것이라고 보면 된다.

싱글톤 레지스트리로서의 애플리케이션 컨텍스트

오브젝트의 동일성과 동등성

자바에서는 두 개의 오브젝트가 같은가라는 말을 주의해서 써야 한다. 자바에서는 두개의 오브젝트가 완전히 같은 동일한 오브젝트라고 말하는 것(동일성)과, 동일한 정보를 담고 있는 오브젝트라고 말하는 것(동등성)은 분명한 차이가 있다. 물론 동일한 오브젝트는 동등하기도 하다.

동일성은 == 연산자로, 동등성은 equals() 메소드를 이용해 비교한다.

스프링의 애플리케이션 컨텍스트는 기존에 직접 만든 오브젝트 팩토리와는 중요한 차이점이 있다. 스프링은 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다는 것이다. 애플리케이션 컨텍스트는 싱글톤을 저장하고 관리하는 싱글톤 레지스트리이기도 하다.

스프링은 기본적으로 별다른 설정을 하지 않으면 내부에서 생성하는 빈 오브젝트를 모두 싱글톤으로 만든다. (디자인 패턴에서 나오는 싱글톤 패턴과 비슷한 개념이지만 그 구현 방법은 확연히 다르다.)

싱글톤 패턴의 한계

일반적인 싱글톤 패턴 구현 방식에는 다음과 같은 문제(한계)가 있다.

  • private 생성자를 갖고 있기 때문에 상속할 수 없다.
  • 싱글톤은 테스트하기가 힘들다.
  • 서버환경에서는 싱글톤이 하나만 만들어지는 것을 보장하지 못한다.
  • 싱글톤의 사용은 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

싱글톤 레지스트리

스프링은 서버환경에서 싱글톤이 만들어져서 서비스 오브젝트 방식으로 사용되는 것은 적극 지지한다. 그러나 자바의 기본적인 싱글톤 패턴의 구현 방식은 여러 가지 단점이 있기 때문에, 스프링은 직접 싱글톤 형태의 오브젝트를 만들고 관리하는 기능을 제공한다. 그것이 바로 싱글톤 레지스트리

싱글톤 레지스트리의 장점은 스태틱 메소드와 private 생성자를 사용해야 하는 비정상적인 클래스가 아니라 평범한 자바 클래스를 싱글톤으로 활용하게 해준다는 것이다.

스프링의 싱글톤 레지스트리 덕분에 싱글톤 방식으로 사용될 애플리케이션 클래스라도 public 생성자를 가질 수 있다.

싱글톤과 오브젝트의 한계

싱글톤은 멀티스레드 환경이라면 여러 스레드가 동시에 접근해서 사용할 수 있다. 따라서 상태 관리에 주의를 기울여야 한다. 기본적으로 싱글톤이 멀티스레드 환경에서 서비스 형태의 오브젝트로 사용되는 경우에는 상태정보를 내부에 갖고 있지 않은 무상태(stateless) 방식으로 만들어져야 한다.

무상태 방식으로 만들기 위해서는 메소드 안에서 생성되는 로컬 변수를 사용하면 된다. 로컬 변수는 매번 새로운 값을 저장할 독립적인 공간이 만들어지기 때문에 싱글톤이라고 해도 여러 스레드 변수의 값을 덮어쓸 일은 없다.

따라서 스프링의 싱글톤 빈으로 사용되는 클래스를 만들 때는 개별적으로 바뀌는 정보는 로컬 변수로 정의하거나, 파라미터로 주고받으면서 사용하게 해야 한다.

그러나, 자신이 사용하는 다른 싱글톤 빈을 저장하려는 용도라면 인스턴스 변수를 사용해도 좋다. 스프링이 한 번 초기화해주고 나면 이후에는 수정되지 않기 때문에 멀티스레드 환경에서 사용해도 아무런 문제가 없다.

스프링 빈의 스코프

스프링이 관리하는 오브젝트, 즉 빈이 생성되고, 존재하고, 적용되는 범위를 빈의 스코프(scope)라고 한다. 스프링 빈의 기본 스코프는 싱글톤이다.

경우에 따라서는 싱글톤 외의 스코프를 가질 수 있다. 그 예로 웹을 통해 새로운 HTTP 요청이 생길 때마다 생성되는 요청(request) 스코프가 있고, 웹의 세션과 스코프가 유사한 세션(session) 스코프도 있다.

의존관계 주입 (DI)

IoC가 매우 느슨하게 정의돼서 폭넓게 사용되는 용어이기 때문에 스프링을 IoC 컨테이너라고만 해서는 스프링이 제공하는 기능의 특징을 명확하게 설명하지 못한다.

그래서 스프링이 제공하는 IoC 방식을 핵심을 짚어주는 의존관계 주입(Dependency Injection)이라는, 좀 더 의도가 명확한 이름을 사용하기 시작했다.

스프링이 다른 프레임워크와 차별화돼서 제공해주는 기능은 의존관계 주입이라는 새로운 용어를 사용할 때 분명하게 드러난다.

DI는 오브젝트 레퍼런스를 외부로부터 제공(주입)받고 이를 통해 다른 오브젝트와 다이내믹하게 의존관계가 만들어지는 것이 핵심이다.

런타임 의존관계 설정

모델이나 코드에서 클래스와 인터페이스를 통해 드러나는 의존관계 말고, 런타임 시에 오브젝트 사이에서 만들어지는 의존관계도 있다. 설계 시점의 의존관계가 실체화된 것이다.

런타임 시에 의존관계를 맺는 대상, 즉 실제 사용대상인 오브젝트를 의존 오브젝트라고 한다.

의존관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체, 보통 클라이언트라고 부르는 오브젝트를 런타임 시에 연결해주는 작업을 말한다.

의존관계 주입이란 다음의 세 가지 조건을 충족하는 작업을 말한다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
  • 이존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관계를 맺도록 도와주는 제3의 존재가 있다는 것이다. 스프링의 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 지닌 제 3의 존재이다.

DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞는다.

의존관계 검색과 주입

스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다.

의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용하기 때문에 의존관계 검색이라고 불리는 것도 있다. 의존관계 검색은 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾는다.

의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트를 결정하는 것과 오브젝트의 생성작업은 외부 컨테이너에게 IoC로 맡기지만, 이를 가져올 때는 메소드나 생성자를 통한 주입 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.

스프링의 IoC와 DI 컨테이너를 적용했다고 하더라도 애플리케이션의 기동 시점에서 적어도 한 번은 의존관계 검색 방식을 사용해 오브젝트를 가져와야 한다. static 메소드인 main()에서는 DI를 이용해 오브젝트를 주입받을 방법이 없기 때문이다.

의존관계 검색(DL)과 의존관계 주입을 적용할 때 발견할 수 있는 중요한 차이점이 하나 있다. 의존관계 검색 방식에서는 검색하는 오브젝트는 자신이 스프링의 빈일 필요가 없다.

반면에 의존관계 주입에서는 오브젝트 사이에 DI가 적용되려면 반드시 두 오브젝트 모두 컨테이너가 만드는 빈 오브젝트여야 한다.

참고

토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리

Spring - MVC

DispatcherServlet 이란?

Spring MVC는 DispatcherServlet의 등장으로 web.xml의 역할이 축소되었습니다. 이전에는 서블릿을 URL로 활용하기 위해서는 반드시 web.xml에 등록해야 했지만, 이제는 DispatcherServlet이 해당 어플리케이션으로 들어오는 요청을 모두 핸들링 해주기 때문입니다.

web.xml의 역할이 축소되었지만, <servlet>으로 DispatcherServlet을 등록해줘야 하며, 이 객체의 URL 적용범위 또한 web.xml에 설정해야 합니다. 또한 encoding과 관련된 <filter><listener>를 등록하기 위해서 web.xml은 필요합니다.

그러나 web.xml에서 중요하게 사용되었던 <servlet> 매핑은 이제 DispatcherServlet이 대신 맡아서 처리하게 되었습니다. web.xml에 DispatcherServlet의 을 ‘/‘로 설정함으로써 동시에 이제 모든 요청은 DispatcherServlet으로 전달됩니다. 물론 DispatcherServlet을 web.xml에 등록해도 계속 서블릿을 web.xml에 매핑해서 사용할 수 있지만, 이런 옛 방식을 버리고 DispatcherServlet을 이용해 웹 개발을 한다면 앞으로 서블릿 파일을 만들 필요도 없어지고 동시에 놀라운 @MVC의 혜택을 얻을 수 있습니다.

DispatcherServlet을 이용한다는 것은 스프링에서 제공하는 @MVC를 이용하겠단 뜻입니다. @MVC는 그동안 추상적으로 알아오고 발전했던 MVC(Model, View, Controller) 설계 영역을 노골적으로 분할하여 사용자가 무조건 MVC로 어플리케이션을 설계하게끔 유도하는 방식입니다. 즉, @MVC를 이용해 어플리케이션을 개발한다면 MVC 설계의 원칙대로 웹 어플리케이션을 제작할 수 있게 된다는 뜻입니다.

그럼 간단하게 DispatcherServlet이 담당하는 역할이 무엇인지 알아봅시다. 먼저 DispatcherServlet에 대해 간단히 정의해보자면, 각각 분리하여 만든 Model, View, Controller를 조합하여 브라우저로 출력해주는 역할을 수행합니다.

Spring MVC 구조

등장 요소

  • DispatcherServlet : 프런트 컨트롤러 담당, 모든 HTTP 요청을 받아들여 그 밖의 오브젝트 사이의 흐름을 제어, 기본적으로 스프링 MVC의 DispatcherServlet 클래스를 그대로 적용
  • HandlerMapping : 클라이언트의 요청을 바탕으로 어느 컨트롤러를 실행할지 결정
  • Model : 컨트롤러에서 뷰로 넘겨줄 오브젝트를 저장하기 위한 오브젝트, HttpServletRequest와 HttpSession처럼 String 형 키와 오브젝트를 연결해서 오브젝트를 유지
  • ViewResolver : View 이름을 바탕으로 View 오브젝트를 결정
  • View : 뷰에 화명 표시 처리를 의뢰
  • 비즈니스 로직 : 비즈니스 로직을 실행. 애플리케이션 개발자가 비즈니스 처리 사양에 맞게 작성
  • 컨트롤러(Controller) : 클라이언트 요청에 맞는 프레젠테이션 층의 애플리케이션 처리를 실행해야 함. 애플리케이션 개발자가 애플리케이션 처리 사양에 맞게 작성
  • 뷰 / JSP 등 : 클라이언트에 대해 화면 표시 처리. 자바에서는 JSP 등으로 작성하는 일이 많으며, 애플리케이션 개발자가 화면의 사양에 맞게 작성

동작 순서

  1. DispatcherServlet은 브라우저로부터 요청을 받아들입니다.
  2. DispatcherServlet은 요청된 URL을 HandlerMapping 오브젝트에 넘기고 호출 대상의 컨트롤러 오브젝트를 얻어 URL에 해당하는 메서드를 실행합니다.
  3. 컨트롤러 오브젝트는 비즈니스 로직으로 처리를 실행하고, 그 결과를 바탕으로 뷰에 전달할 오브젝트를 Model 오브젝트에 저장합니다. 끝으로 컨트롤러 오브젝트는 처리 결과에 맞는 View 이름을 반환합니다.
  4. DispatcherServlet은 컨트롤러에서 반환된 View 이름을 ViewResolver에 전달해서 View 오브젝트를 얻습니다.
  5. DispatcherServlet은 View 오브젝트에 화면 표시를 의뢰합니다.
  6. View 오브젝트는 해당하는 뷰를 호출해서 화면 표시를 의뢰합니다.
  7. 뷰는 Model 오브젝트에서 화면 표시에 필요한 오브젝트를 가져와 화면 표시 처리를 실행합니다.

참고

Spring - RequestBody & ResponseBody

웹 서비스와 RESTful한 방식이 시스템을 구성하는 주요 요소로 자리 잡으면서 웹 시스템간에 XML이나 JSON 등의 형식으로 데이터를 주고 받는 경우가 증가했습니다.

이에 따라 스프링 MVC도 **클라이언트에서 전송한 XML 데이터나 JSON 또는 기타 데이터를 컨트롤러에서 DOM 객체나 자바 객체로 변환해서 받을 수있는 기능(수신)**을 제공하고 있으며, 비슷하게 **자바 객체를 XML이나 JSON 또는 기타 형식으로 변환해서 전송할 수 있는 기능(송신)**을 제공하고 있습니다.

@RequestBody 어노테이션과 @ResponseBody 어노테이션은 각각 HTTP 요청의 body 부분을 자바 객체로 변환하고 자바 객체를 HTTP 응답 body로 변환하는데 사용됩니다.

@RequestBody

Spring MVC 컨트롤러에서 HTTP 요청의 body 부분을 자바 객체로 mapping할 때 @RequestBody 어노테이션을 사용합니다. @RequestBody 어노테이션의 기능은 다음과 같습니다.

  • @ReuqestBody를 사용하지 않는 경우 : query parameter, form data를 객체에 mapping한다.
  • @ReuqestBody를 사용하는 경우 : body에 있는 data를 HttpMessageConverter를 이용해 선언한 객체에 mapping한다.

@ResponseBody

@ResponseBody는 @RequestBody와 비슷한 방식으로 동작합니다. @ResponseBody가 메소드 레벨에서 부여되면 메소드가 리턴하는 오브젝트는 ContentNegotiatingViewResolver를 이용해 뷰를 통해 결과를 만들어내는 것이 아닌, message converter를 통해 바로 HTTP 응답의 메시지 본문으로 변환됩니다.

ContentNegotiatingViewResolver는 등록되어 있는 ViewResolver중에서 controller 메소드의 리턴값을 통해 등록된 ViewResolver 중에서 적합한 형태로 처리해서 반환하는 반면, @ResponseBody는 @RequestBody가 선택한 형식으로 결과값을 변환하여 반환한다고 보면 됩니다.

@RestController는 @Controller와 @ResponseBody를 동시에 사용하는 것과 같습니다. @Controller를 사용하는 경우에만 @ResponseBody를 추가하면 됩니다.

HttpMessageConverter를 이용한 변환 처리

AnnotationMethodHandlerAdapter에는 HttpMessageConverter 타입의 메시지 변환기인 message converter가 여러 개 등록되어 있습니다. @RequestBody가 붙은 파라미터가 있으면 HTTP 요청의 미디어 타입과 파라미터의 타입을 먼저 확인하고, message converter 중에서 해당 미디어 타입과 파라미터 타입을 처리할 수 있다면, HTTP 요청의 body 부분을 통째로 변환해서 지정된 메소드 파라미터로 전달해줍니다.

HttpMessageConverter의 종류

AnnotationMethodHandlerAdapter 클래스는 @RequestBody 어노테이션이 적용된 파라미터나 @ResponseBody 어노테이션이 적용된 메서드에 대해 HttpMessageConverter를 사용해서 변환을 처리합니다. 주요 HttpMessageConverter 구현 클래스는 다음과 같습니다.

  • ByteArrayHttpMessageConverter : HTTP 메시지와 byte 배열 사이의 변환을 처리한다. 컨텐츠 타입은 application/octet-stream이다.
  • StringHttpMessageConverter : HTTP 메시지와 String 사이의 변환을 처리한다. 컨텐츠 타입은 text/plain;charset=ISO-8859-1이다.
  • SourceHttpMessageConverter : HTTP 메시지와 javax.xml.transform.Source 사이 변환을 처리한다. 컨텐츠 타입은 application/xml 또는 text/xml이다.
  • FormHttpMessageConverter : HTML 폼 데이터를 MultiValueMap으로 전달받을 때 사용된다. 지원하는 컨텐츠 타입은 application-x-www-form-urlencorded이다.
  • MappingJacksonHttpMessageConverter : Jackson 라이브러리를 이용해서 JSON HTTP 메시지와 객체 사이의 변환을 처리한다. 컨텐츠 타입은 applicaion/json이다.
  • MarshallingHttpMessageConverter : 스프링의 Marshaller와 unMarshaller를 이용해서 XML HTTP 메시지와 객체 사이의 변환을 처리한다. 컨텐츠 타입은 application/xml 또는 text/xml이다.

Content-Type과 Accept header 기반의 변환 처리

AnnotationMethodHandlerAdapter가 HttpMessageConverter를 이용해서 request의 body 데이터를 @RequestBody 어노테이션이 적용된 자바 객체로 변환할 때에는, HTTP 요청 header의 Content-Type에 명시된 미디어 타입(MIME)을 지원하는 HttpMessageConverter 구현체를 선택합니다.
예를 들어, 요청 미디어 타입이 application/json이고 @RequestBody 어노테이션이 적용된 경우 MappingJacksonHttpMessageConverter가 선택됩니다.

비슷하게 @ResponseBody 어노테이션을 이용해서 리턴한 객체를 HTTP 응답 객체로 변환할 때에는 HTTP 요청 header의 Accept에 명시된 미디어 타입(MIME)을 지원하는 HttpMessageConveter 구현체를 선택합니다.
예를 들어, Accept에 명시된 값이 application/json이고 @ResponseBody 어노테이션이 적용된 메서드의 리턴 타입이 자바 객체인 경우 MappingJacksonHttpMessageConverter가 선택됩니다.

참고

Http Message Converters with the Spring Framework

Spring - AOP

AOP의 등장 배경

몇 년에 걸쳐 객체지향 프로그래밍(Object Oriented Programming, OOP)은 절차적 프로그래밍 방법론을 거의 완벽히 대체하며 프로그래밍 방법론의 새로운 패러다임으로 떠오르게 되었습니다. 객체지향적 방식의 가장 큰 이점 중 하나는 소프트웨어 시스템이 여러 개의 독립된 클래스들의 집합으로 구성된다는 것입니다. 이들 각각의 클래스들은 잘 정의된 고유 작업을 수행하게 되고, 그 역할 또한 명백히 정의되어 있습니다.

객체지향 어플리케이션에서는 어플리케이션이 목표한 동작을 수행하기 위해 이런 클래스들이 서로 유기적으로 협력합니다. 하지만 시스템의 어떤 기능들은 특정 한 클래스가 도맡아 처리할 수 없습니다. 이들은 시스템 전체에 걸쳐 존재하며 해당 코드들을 여러 클래스들에서 사용합니다. 이런 현상을 횡단적(cross-cutting)이라 표현합니다. 분산 어플리케이션에서의 동기화(locking) 문제, 예외 처리, 로깅 등이 그 예입니다. 물론 필요한 모든 클래스들에 관련 코드를 집어 넣으면 해결될 문제입니다. 하지만 이런 행위는 각각의 클래스는 잘 정의된(well-defined) 역할만을 수행한다는 기본 원칙에 위배됩니다. 이런 상황이 바로 Aspect Oriented Programming (AOP)가 생겨난 원인이 되었습니다.

AOP에서는 aspect라는 새로운 프로그램 구조를 정의해 사용합니다. 쉽게 class, interface 등과 같이 특정한 용도의 구조라 생각하면 됩니다. Aspect 내에는 프로그램의 여러 모듈들에 흩어져 있는 기능(하나의 기능이 여러 모듈에 흩어져 있음을 뜻함)을 모아 정의하게 됩니다. 전체적으로, 어플리케이션의 각각의 클래스는 자신에게 주어진 기능만을 수행하고, 추가된 각 aspect들이 횡단적인 행위(기능)들을 모아 처리하며 전체 프로그램을 이루는 형태가 만들어집니다.

AOP가 필요한 사례

이해를 돕기 위해 어플리케이션의 여러 스레드들이 하나의 데이터를 공유하는 상황을 가정해봅시다. 공유 데이터는 Data라는 객체(Data 클래스의 인스턴스)로 캡슐화되어 있습니다. 서로 다른 여러 클래스의 인스턴스들이 하나의 Data 객체를 사용하고 있으나, 이 공유 데이터에 접근할 수 있는 객체는 한 번에 하나씩이어야만 합니다. 그렇다면 어떤 형태이건 동기화 메커니즘이 도입되어야 할 것입니다. 즉, 어떤 한 객체가 데이터를 사용중이라면 Data 객체는 잠겨(lock)져야 하며, 사용이 끝났을 때 해제(unlock)되어야 합니다. 전통적인 해결책은 공유 데이터를 사용하는 모든 클래스들이 하나의 공통 부모 클래스(“worker” 라 부르겠습니다)로부터 파생되는 형태로 만드는 것입니다. worker 클래스에는 lock()과 unlock() 메소드를 정의하여 작업의 시작과 끝에 이 메소드를 호출토록 하면 됩니다. 하지만 이런 형태는 다음과 문제들을 파생시킵니다.

공유 데이터를 사용하는 메소드는 상당히 주의해서 작성되어야 합니다. 동기화 코드를 잘못 삽입하면 데드락(dead-lock)이 발생하거나 데이터 영속성이 깨질 수 있습니다. 또한 메소드 내부는 본래의 기능과 관련 없는 동기화 관련 코드들로 더럽혀질 것입니다.
Java와 같은 단일 상속 모델에서는 worker를 만든다는 것이 불가능할 수 있습니다. 어떤 클래스들은 이미 다른 클래스들로부터 확장되었을 수도 있기 때문입니다. 이는 특히 클래스 계층 구조 설계가 마무리된 후, 뒤늦게 동기화의 필요성을 깨달았을 때 흔히 발생합니다. 동기화를 신경 쓰지 않은 범용 클래스 라이브러리를 통해 공유 데이터에 접근하려 하는 경우가 한 예가 될 수 있습니다.
앞서 가정한 어플리케이션에서 동기화 개념은 다음과 같은 속성들을 갖습니다.

  1. 동기화는 worker 클래스에 할당된 최우선 작업이 아니다.
  2. 동기화 메커니즘은 worker 클래스의 최우선 작업과 독립적이다.
  3. 한 객체에 대한 동기화 관련 코드가 시스템 전체에 횡단적으로 존재한다. 다수의 클래스와 더 많은 수의 메소드들이 이 동기화 메커니즘에 영향 받는다.

AOP에서는 이런 형태의 문제를 해결하기 위해 새로운 형태의 접근 방법을 제기하고 있습니다. AOP는 새로 도입된 프로그램 구조를 통해 시스템에 횡단되어 있는 기능들을 정의해 처리하도록 했습니다. 이 새로운 구조를 aspect라 부릅니다.

위의 예시에 Lock이라는 aspect를 도입해보겠습니다. Lock aspect에는 다음과 같은 역할이 할당될 것입니다.

  1. Data 객체를 사용하는 클래스들을 위해 lock 및 unlock 메커니즘을 제공한다(lock(), unlock()).
  2. Data 객체를 수정하는 모든 메소드들이 수행 전에 lock()을 호출하고, 수행 후에는 unlock()을 호출함을 보장한다.
  3. 이상의 기능을 Data 객체를 사용하는 클래스의 자바 소스를 변경하지 않고 투명하게 수행한다.

Aspect는 또 어떤 일들을 수행할 수 있을까?

특정 메소드(ex. 객체 생성 과정 추적) 호출을 로깅할 경우 aspect가 도움이 될 수 있습니다. 기존 방법대로라면 log() 메소드를 만들어 놓은 후, 자바 소스에서 로깅을 원하는 메소드를 찾아 log()를 호출하는 형태를 취해야할 것입니다. 그러나 여기서 AOP를 사용하면 원본 자바 코드를 수정할 필요 없이 원하는 위치에서 원하는 로깅을 수행할 수 있습니다. 이런 작업 모두는 aspect라는 외부 모듈에 의해 수행됩니다.
또 다른 예로 예외 처리가 있습니다. Aspect를 이용해 여러 클래스들의 산재된 메소드들에 영향을 주는 catch() 조항(clause)을 정의해 어플리케이션 전체에 걸쳐 지속적이고 일관적으로 예외를 처리할 수 있습니다.

AOP 용어

  • JoinPoint : 메소드 호출이나 특정 예외를 던지는 것과 프로그램이 실행되는 지점을 이야기한다.
  • Advice : Logging과 같은 횡단관심사의 경우 거의 모든 클래스에 분산되어 있는 것을 볼 수 있다. 이와 같은 횡단관심사를 여러 영역에 분산해 구현하는 것이 아니라 한 곳에 모아서 구현하는 것을 Advice라고 한다. 즉, JoinPoint에서 실행되는 코드를 말한다.
  • Point-cut : 횡단관심사에 해당하는 기능을 구현한 부분이 Advice라고 했다. 그렇다면 이렇게 구현되어 있는 Advice를 어떤 패턴을 가지는 클래스와 메소드에 적용할지를 결정하는 것이 Point-cut이다. 즉 해당 Advice가 적용되어야 하는 곳을 가리키는 것이 Point-cut이다. Point-cut은 JoinPoin와 Advice의 중간에 있으면서 처리가 JoinPoint에 이르렀을 때 Advice를 호출할지를 결정한다.
  • Aspect : Aspect는 Advice와 Point-cut을 합쳐서 하나의 Aspect라고 칭한다. Advice와 Point-cut을 이용하여 Logging이라는 관심사를 분리하여 독립적으로 구현할 수 있었다. 이처럼 Advice와 Point-cut을 이용하여 원하는 관심사를 구현하는 것을 하나의 Aspect라고 한다. 지금까지 살펴본 Logging은 Logging Aspect가 될 것이다.
  • Introduction : 실행되고 있는 클래스에 새로운 인터페이스를 추가하여 원래의 Object가 가지고 있는 속성, 행위 이외의 다른 일이 가능하도록 하게 된다.

Spring AOP의 Advice는 여러개의 Advice를 가집니다. Spring에서 지원하고 있는 Advice는 다음과 같습니다.

  • Before advice : JoinPoint 앞에서 수행되는 Advice. 하지만 JoinPoint를 위한 수행 흐름 처리(execution flow proceeding)를 막기위한 능력(만약 예외를 던지지 않는다면)을 가지지는 않는다.
  • After returning advice : JoinPoint가 완전히 정상 종료한 다음 실행되는 Advice. (메소드가 예외를 던지는것 없이 반환된다면 완성된 후에 수행되는 advice.)
  • Around advice : JoinPoint 앞뒤에서 실행되는 Advice. Around advice는 메소드 호출 전후에 사용자 지정 행위를 수행한다.
  • Throws advice : JoinPoint에서 예외가 발생했을 때 실행되는 Advice. Spring은 강력한 타입의 Throws advice를 제공한다. 그래서 Throwable 나 Exception으로 부터 형변환 할 필요가 없는 관심가는 예외(그리고 하위클래스)를 처리하는 코드를 쓸 수 있다.

Spring - IoC & DI

IoC란?

IoC 컨테이너 개념을 이해하기 위하여 이와 같은 컨테이너가 왜 등장하게 되었는지를 먼저 이해하는 것이 중요합니다.

애플리케이션 코드를 작성할 때, 특정 기능이 필요하면 라이브러리 사용하곤 합니다. 이때는 프로그램의 흐름을 제어하는 주체가 애플리케이션 코드입니다. 하지만 프레임워크(Framework) 기반의 개발에서는 프레임워크 자신이 흐름을 제어하는 주체가 되어, 필요 할 때마다 애플리케이션 코드를 호출하여 사용합니다.

프레임워크에서 이 제어권을 가지는 것이 바로 컨테이너(Container)입니다. 객체에 대한 제어권이 개발자로부터 컨테이너에게 넘어가면서 객체의 생성부터 생명주기 관리까지의 모든 것을 컨테이너가 맡아서 하게됩니다. 이를 일반적인 제어권의 흐름이 바뀌었다고 하여 IoC(Inversion of Control : 제어의 역전)라고 합니다.

먼저 지금까지 일반적으로 개발하던 방식에 대해서 생각해보아야 합니다. 모든 인스턴스에 대한 생성 권한은 지금까지 모든 개발자들에게 있었습니다. 즉, 작성하는 코드상에서 개발자가 직접 생성했다는 것입니다. EJB나 IoC 컨테이너를 사용하지 않았던 개발자들은 지금까지 이와 같은 방식을 사용했습니다.

EJB는 각 개발자들이 모든 인스턴스의 생성 권한에 제약을 가하는 첫번째 프레임워크입니다. EJB는 서비스를 위해 생성되는 컴포넌트에 대한 생성 권한을 EJB 컨테이너에게 위임했습니다. 생성된 인스턴스는 EJB 컨테이너가 생명주기를 관리했습니다. EJB가 EJB 컨테이너에 의하여 관리됨으로 인해 큰 장점을 얻을 수 있었습니다. 그러나 장점 이외에 EJB가 가지고 있는 한계에 부딪히게 되었으며, 이 같은 요구사항을 해결하기 위해 EJB의 한계를 극복하기 위한 시도가 발생했습니다.

그래서 등장한 것이 경량(LightWeight) IoC 컨테이너 입니다. 경량 IoC 컨테이너는 EJB 컨테이너가 가지고 있던 단점을 보완하기 위하여 탄생한 컨테이너 개념입니다. Spring 프레임워크에서 지원하는 IoC 컨테이너는 우리들이 흔히 개발하고 사용해왔던 일반 POJO(Plain Old Java Object) 클래스들이 지금까지 EJB를 통하여 실행했던 많은 기능들을 서비스 가능하도록 지원합니다.
또한, EJB 컨테이너가 지원하고 있던 Transaction, Object Pooling, 인스턴스 생명주기 관리등의 기능들을 Spring 컨테이너가 지원하며 부가적으로 테스트의 용이성(애플리케이션 품질의 향상), 개발 생산성을 향상 시킬 수 있습니다.

사용하는 목적

IoC를 사용하는 목적에 대해서는 지금까지의 클래스호출 방식의 변화를 살펴보면 더 쉽게 이해할 수 있습니다.

클래스 호출 방식

클래스내에 선언과 구현이 같이 있기 때문에 다양한 형태로 변화가 불가능합니다.

인터페이스 호출 방식

클래스를 인터페이스와 인터페이스를 상속받아 구현하는 클래스로 분리했습니다. 구현클래스 교체가 용이하여 다양한 변화가 가능합니다. 그러나 구현클래스 교체시 호출클래스의 코드에서 수정이 필요합니다. (부분적으로 종속적)

팩토리 호출 방식

팩토리 방식은 팩토리가 구현클래스를 생성하기 때문에 호출클래스는 팩토리를 호출 하는 코드로 충분합니다. 구현클래스 변경시 팩토리만 수정하면 되기 때문에 호출클래스에는 영향을 미치지 않습니다. 그러나 호출클래스에서 팩토리를 호출하는 코드가 들어가야 하는 것 또한 팩토리에 의존함을 의미합니다.

IoC

팩토리 패턴의 장점을 더해 어떠한 것에도 의존하지 않는 형태가 되었습니다. 실행시점에 클래스간의 관계가 형성이 됩니다. 즉, 의존성이 삽입된다는 의미로 IoC를 DI라는 표현으로 사용합니다.

IoC 용어 정리

  • bean : 스프링에서 제어권을 가지고 직접 만들어 관계를 부여하는 오브젝트
    Java Bean, EJB의 Bean과 비슷한 오브젝트 단위의 애플리케이션 컴포넌트이다. 하지만 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 빈은 아니다. 스프링의 빈은 스프링 컨테이너가 생성하고 관계설정, 사용을 제어해주는 오브젝트를 말한다.
  • bean factory : 스프링의 IoC를 담당하는 핵심 컨테이너
    Bean을 등록/생성/조회/반환/관리 한다. 보통 bean factory를 바로 사용하지 않고 이를 확장한 application context를 이용한다. BeanFactory는 bean factory가 구현하는 interface이다. (getBean()등의 메서드가 정의되어 있다.)
  • application context : bean factory를 확장한 IoC 컨테이너
    Bean의 등록/생성/조회/반환/관리 기능은 bean factory와 같지만, 추가적으로 spring의 각종 부가 서비스를 제공한다. ApplicationContext는 application context가 구현해야 하는 interface이며, BeanFactory를 상속한다.
  • configuration metadata : application context 혹은 bean factory가 IoC를 적용하기 위해 사용하는 메타정보
    스프링의 설정정보는 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만 주로 bean을 생성/구성하는 용도로 사용한다.
  • container (ioC container) : IoC 방식으로 bean을 관리한다는 의미에서 bean factory나 application context를 가리킨다.
    application context는 그 자체로 ApplicationContext 인터페이스를 구현한 오브젝트를 말하기도 하는데, 하나의 애플리케이션에 보통 여러개의 ApplicationContext 객체가 만들어진다. 이를 통칭해서 spring container라고 부를 수 있다.

스프링을 사용하지 않을 때 일어날 수 있는 문제

스프링의 특징을 알아보기 앞서 스프링을 사용하지 않을 때 어떤 문제가 일어날 수 있는지 알아보겠습니다.

  • 오브젝트의 생명 주기 문제
  • 부품화 문제
  • 기술 은닉과 부적절한 기술 은닉 문제

이러한 문제를 해결하지 않는 한 웹 애플리케이션은 리소스를 잘 이용하지 못하고, 테스트하기 어려우며, 확장이나 변경 또한 어려울 것입니다. 스프링은 이러한 문제를 해결하기 위해 만들어진 컨테이너라고도 할 수 있습니다.
스프링은 위의 문제를 다음과 같이 해결합니다.

  • 오브젝트의 생명 주기 문제는 DI 컨테이너로 해결
  • 부품화 문제는 DI 컨테이너로 해결
  • 기술 은닉과 부적절한 기술 은닉 문제는 AOP로 해결

DI

IoC는 직관적이지 못하기 때문에 DI(Dependency Injection)라고도 부릅니다. DI는 오브젝트를 생성하고 오브젝트끼리의 관계를 생성해 소프트웨어의 부품화 및 설계를 가능하게 합니다. DI를 이용하면 인터페이스 기반의 컴포넌트를 쉽게 구현할 수 있습니다.
DI를 우리말로 옮기면 의존 관계의 주입입니다. 쉽게 말하면 오브젝트 사이의 의존 관계를 만드는 것입니다. 어떤 오브젝트의 프로퍼티(인스턴스 변수)에 오브젝트가 이용할 오브젝트를 설정한다는 의미입니다. 이를 학술적으로 말하면, 어떤 오브젝트가 의존(이용)할 오브젝트를 주입 혹은 인젝션(프로퍼티에 설정)한다는 것입니다.
DI를 구현하는 컨테이너는 단순한 인젝션 외에도 클래스의 인스턴스화 등의 생명 주기 관리 기능이 있는 경우가 많습니다.

클래스에서 new 연산자가 사라졌다는 사실이 중요합니다. 클래스에서 new 연산자가 사라짐으로써 개발자가 팩토리 메서드 같은 디자인 패턴을 구사하지 않아도 DI 컨테이너가 건내주는 인스턴스를 인터페이스로 받아서 인터페이스 기반의 컴포넌트화를 구현할 수 있게 됐습니다.

DI 컨테이너의 구상 클래스 인스턴스화는(디폴트로는) 1회만 실행합니다. 생성된 인스턴스는 필요한 곳에서 사용합니다. 이렇게 하는 것으로 서비스와 DAO처럼 Singleton으로 만들고 싶은 컴포넌트를 특별히 Singleton으로 만들지 않아도 간단히 실현되게 해줍니다.

스프링에는 크게 (1)XML로 작성된 Bean 정의 파일을 이용한 DI, (2)어노테이션을 이용한 DI, (3)JavaConfig에 의한 DI가 있습니다. 이번 포스팅에서는 어노테이션을 이용한 DI에 대해 알아보겠습니다.

@Autowired와 @Component

인스턴스 변수 앞에 @Autowired를 붙이면 DI 컨테이너가 그 인스턴스 변수의 형에 대입할 수 있는 클래스를 @Component가 붙은 클래스 중에서 찾아내 그 인스턴스를 인젝션해줍니다(정확히는 Bean 정의에서 클래스를 스캔할 범위를 정해야 합니다).
인스턴스 변수로의 인젝션은 접근 제어자가 private라도 인젝션 할 수 있으므로 Setter 메서드를 만들 필요는 없습니다. (과거에 캡슐화의 정보 은닉에 반하는 것이 아니냐는 논의가 있었지만, 현재는 편리함에 밀려 그런 논의를 보기 힘들어졌습니다.)

만약 @Component가 붙은 클래스가 여러 개 있어도 형이 다르면 @Autowired가 붙은 인스턴스 변수에 인젝션되지 않습니다. 이렇게 형을 보고 인젝션하는 방법을 byType이라고 합니다.

@Autowired

@Autowired는 인스턴스 변수 앞에 붙이는 것 외에도, 다음과 같이 적당한 메서드 선언 앞에도 붙일 수 있습니다.

1
2
3
4
5
6
7
8
9
@Autowired
public void setFoo(Foo foo) {
this.foo = foo;
}
@Autowired
public void setFoo(Foo foo, Bar bar) {
this.foo = foo;
this.bar = bar;
}

또한 생성자에도 이용할 수 있습니다.

1
2
3
4
public class Foo {
@Autowired
public Food(Bar b) {...}
}


그런데 위의 사진과 같이 인터페이스에 구현 클래스가 2개여서 @Autowired로 인젝션할 수 있는 클래스의 형이 2개 존재한다면 에러가 발생합니다. 인젝션할 수 있는 클래스의 형은 반드시 하나로 해야합니다. 하지만 이래서는 인터페이스의 구현 클래스를 테스트용 클래스 등 다른 클래스로 바꿀 경우에 불편합니다. 그래서 이를 회피하는 세 가지 방법에 대해 알아보겠습니다.

  1. 우선할 디폴트 Bean을 설정하는 @Primary를 @Bean이나 @Component에 부여하는 방법
    (Bean 정의 파일에서는 )
    1
    2
    3
    4
    5
    @Component
    @Primary
    public class ProductDaoImpl implements ProductDao {
    ...(생략)...
    }
  2. @Autowired와 병행해서 @Qualifier를 하는 방법
    단, 이 경우는 @Component에도 이름을 같이 지정해야 한다. 이렇게 인젝션할 클래스를 형이 아닌 이름으로 찾아주는 방법을 byName이라고 한다. (물론 @Component에 같은 이름이 붙은 클래스가 중복되면 오류가 발생한다.)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Autowired
    @Qualifier("productDao")
    private ProductDao productDao;

    -----------------------------------------

    @Component("productDao")
    public class ProductDaoImpl implements ProductDao {
    ...(생략)...
    }
  3. Bean 정의 파일인 context:component-scan을 이용하는 방법
    (context:component-scan을 어느 정도 크기의 컴포넌트마다 기술해두고, 만약 어떤 컴포넌트를 테스트용으로 바꾸고자 할 때는 그 컴포넌트 부분의 정의만 테스트용 부품을 스캔하게 수정하는 방법이다.)

확장된 @Component

@Component에는 확장된 어노테이션이 있습니다. 웹 애플리케이션 개발에는 @Component를 이용할 것이 아니라 클래스가 어느 레이어에 배치될지 고려해서 배치될 레이어에 있는 @Component 확장 어노테이션을 사용하는 것이 좋습니다. 예를 들어 ProductServiceImpl은 @Component가 아니라 @Service로 바꾸는 편이 좋고, ProductDaoImpl 클래스도 @Component가 아니라 @Repository로 바꾸는 편이 좋습니다.

  • @Controller : 프레젠테이션 층 스프링 MVC용 어노테이션
  • @Service : 비즈니스 로직 층 Service용 어노테이션, @Component와 동일
  • @Repository : 데이터 엑세스 층의 DAO용 어노테이션
  • @Configuration : Bean 정의를 자바 프로그램에서 실행하는 JavaConfig용 어노테이션

@Component와 함께 사용하는 어노테이션의 하나로 @Scope가 있습니다. @Scope 뒤에 Value 속성을 지정하면 인스턴스화와 소멸을 제어할 수 있습니다. @Scope를 생략하면 해당 클래스는 싱글턴이 됩니다.

1
2
3
4
5
@Component
@Scope("singletone")
public class ProductDaoImple implements ProductDao {
...(생략)...
}
  • singleton : 인스턴스를 싱글턴으로 함
  • prototype : 이용할 때마다 인스턴스화함
  • request : Servlet API의 request 스코프인 동안만 인스턴스가 생존함
  • session : Servlet API의 session 스코프인 동안만 인스턴스가 생존함
  • application : Servlet API의 application 스코프인 동안만 인스턴스가 생존함

value 속성의 값은 직접 문자열로 넣어도 되지만, 상수가 존재하기 때문에 상수를 사용하는 것이 좋습니다.

  • singleton : BeanDefinition.SCOPE_SINGLETON
  • prototype : BeanDefinition.SCOPE_PROTOTYPE
  • request : WebApplicationContext.SCOPE_REQUEST
  • session : WebApplicationContext.SCOPE_SESSION
  • application : WebApplicationContext.SCOPE_APPLICATION

생명 주기 관리

스프링 DI 컨테이너에는 인스턴스의 생성과 소멸 타이밍에 호출되는 메서드를 설정할 수 있는 @PosetConstruct와 @PreDestroy라는 2개의 어노테이션이 있습니다.

  • @PostConstruct : 초기 처리를 하는 메서드 선언. 메서드 이름은 임의로 지정할 수 있다. 단, 메서드 인수 없이 반환형은 void 형으로 해야한다.
  • @PreDestroy : 종료 처리를 하는 메서드 선언. 메서드 이름은 임의로 지정할 수 있다. 단, 메서드 인수 없이 반환형은 void 형으로 해야한다.

@PostConstruct는 DI 컨테이너에 의해 인스턴스 변수에 무언가 인젝션된 다음에 호출됩니다. 따라서 인젝션 된 값으로 초기 처리를 할 때 사용합니다. (생성자에서도 초기 처리를 할 수 있습니다.)
@PreDestroy는 소멸자가 없는 자바에서 종료 처리를 하기 위해 사용합니다.

참고

IntelliJ에서 SpringBoot 프로젝트 생성하기

서론

Spring은 J2EE나 JEE로 알려진 자바 엔터프라이즈 에디션을 경량화하기 위해 시작되었다. 스프링은 무거운 엔터프라이즈 자바 빈(EJB)로 컴포넌트를 개발하지 않았다. 그 대신 의존성 주입(DI)과 관점 지향 프로그래밍(AOP)을 활용해서 EJB의 기능을 평범한 자바 객체(POJO)로 구현할 수 있게 하여 간단하게 엔터프라이즈 자바 개발에 접근할 수 있도록 했다.
컴포넌트 코드 작성은 가벼워졌지만, Spring Framework 기반의 웹 프로젝트를 진행하게되면 최초 개발 구성(설정)하는 부분에 많은 시간이 소모되었다. 결국 애플리케이션 로직 작성이 아닌 프로젝트 구성 작업에 쓰는 시간이 많이 Gk소모되는 것이다.
SpringBoot는 Spring의 복잡한 설정을 최소화하여 빠르게 프로젝트 개발을 시작할 수 있게 해준다. 이 포스팅에서는 IntelliJ에서 SpringBoot로 웹 프로젝트를 시작하는 방법을 소개하며 SpringBoot의 특징을 소개한다.

IntelliJ에서 SpringBoot 프로젝트 생성

Spring Initializr는 SpringBoot 프로젝트 구조를 만드는 웹 애플리케이션이다. 기본적인 프로젝트 구조와 코드를 빌드하는 데 필요한 maven이나 gradle 빌드 명세를 만들어준다. 그러므로 Spring Initializr가 만든 프젝트에 애플리케이션 코드만 작성하면 된다.
Spring Initializr는 웹 기반 인터페이스, Spring Tool Suite(STS), IntelliJ IDE, SpringBoot CLI로 사용할 수 있다. 그 중 IntelliJ를 사용해 프로젝트를 생성해보자.

IntelliJ를 시작하여 Create New Project를 선택하고 새로운 프로젝트 다이얼로그를 연다. New Project 다이얼로그에서 Spring Initializr 프로젝트를 선택하고 자바 SDK를 설정한 후 Next 버튼을 누른다.

두 번째 화면에서는 프로젝트 이름, 빌드할 때 maven과 gradle 중 어느 것을 사용할지, 자바 버전 등 프로젝트의 기본적인 사항을 물어본다. 프로젝트 정보를 입력하고 Next 버튼을 누른다.

세 번째 화면에서는 프로젝트에서 필요한 종류의 의존성을 추가한다. Web, Thymeleaf, JPA, H2를 선택한 후 Next 버튼을 누른다.

다음으로 프로젝트가 저장되는 경로를 지정한다.

Gradle 설정을 지정한다.

코드 작성

도메인 정의 (Diary.java)

src/main/java/com.example.demo/Diary.java 파일을 작성한다.
일기를 나타내는 엔티티 정의한다. 간단하게 id, title, ocntent 필드를 갖고 있는 POJO 객체로 만든다. @Entity 어노테이션을 붙여 클래스를 JPA 엔티티로 지정했고, id 필드에는 @Id@GeneratedValue 어노테이션을 붙여 엔티티의 유일성을 식별하고 자동으로 값을 제공하는 필드로 지정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.example.demo;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Diary {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String user;
private String title;
private String content;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getUser() {
return user;
}

public void setUser(String user) {
this.user = user;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public String getContent() {
return content;
}

public void setContent(String content) {
this.content = content;
}
}

레파지토리 인터페이스 선언 (DiaryListRepository.java)

src/main/java/com.example.demo/DiaryListRepository.java 파일을 작성한다.
데이터베이스에 Diary 객체를 저장할 수 있는 레파지토리를 선언한다. 스프링 JPA를 사용하므로 스프링 데이터 JAP의 인터페이스를 상속하여 인터페이스를 만든다. JpaRepository 인터페이스는 타입 매개변수 두 개를 받는다. 첫 번째는 레파지토리가 사용할 도메인 타입, 두번 째는 클래스의 ID 프로퍼티 타입이다. 지정한 유저의 이름으로 도서 목록을 검색하는 findByUser() 메서드를 추가했다.
DiaryListRepository는 JpaRepository 인터페이스를 상속받아 18개의 메서드를 구현해야 한다. 그러나 스프링 데이터는 레파지토리를 인터페이스로 정의만 해도 잘 작동할 수 있게 런타임 시에 자동으로 구현해준다.

1
2
3
4
5
6
7
8
9
10
package com.example.demo;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;

public interface DiaryListRepository extends JpaRepository<Diary, Long> {

List<Diary> findByUser(String user);
}

일기 목록 애플리케이션의 스프링 MVC 컨트롤러 (DiaryListController.java)

src/main/java/com.example.demo/DiaryListController.java 파일을 작성한다.
클래스에 @Controller 어노테이션을 추가하면, 자동 컴포넌트 검색으로 DiaryListController를 발견해 자동으로 스프링 애플리케이션 컨텍스트에 빈으로 등록한다. 요청을 처리하는 모든 메서드를 기본 URL 경로인 /로 매핑하기 위해 @RequestMapping 어노테이션을 붙였다.
usersDiarys() 메서드는 “diaryList”를 논리적 뷰 이름으로 반환한다. 그러므로 이 뷰도 만들어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demo;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/")
public class DiaryListController {

private static final String user="jongmin";

private DiaryListRepository diaryListRepository;

@Autowired
public DiaryListController(DiaryListRepository diaryListRepository) {
this.diaryListRepository = diaryListRepository;
}

@RequestMapping(method= RequestMethod.GET)
public String usersDiarys(Model model) {
List<Diary> diaryList = diaryListRepository.findByUser(user);
if (diaryList != null) {
model.addAttribute("diarys", diaryList);
}
return "diaryList";
}

@RequestMapping(method = RequestMethod.POST)
public String addToReadingList(Diary diary) {
diary.setUser(user);
diaryListRepository.save(diary);
return "redirect:/";
}
}

일기 목록을 보여주는 Thymeleaf 탬플릿 (diaryList.html)

src/main/resources/template/diaryList.html 파일을 작성한다.
유저의 일기 목록 부분과 일기를 일기 목록에 추가할 때 사용하는 입력 폼을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.springframework.org/schema/data/jaxb">
<head>
<meta charset="UTF-8" />
<title>일기 리스트</title>
</head>
<body>
<h2>일기 목록</h2>
<div th:unless="${#lists.isEmpty(diarys)}">
<dl th:each="diary : ${diarys}">
<dt>
<span th:if="${diary.title}" th:text="${diary.title}">Title</span>
</dt>
<dd>
<span th:if="${diary.content}" th:text="${diary.content}">Content</span>
</dd>
</dl>
</div>

<hr />

<h3>일기 작성</h3>
<form method="POST" th:action="@{/}">
<label for="title">Title:</label>
<input type="text" name="title" size="50" /><br />
<label for="content">Content:</label>
<input type="text" name="content" size="100" /><br />
<input type="submit" value="추가" />
</form>
</body>
</html>

실행 결과

SpringBoot 특징 살펴보기

SpringBoot를 이용해 간단한 애플리케이션을 만들어 보았다. 이 애플리케이션을 바탕으로 SpringBoot의 특징을 알아보자.

스타터 의존성

처음 프로젝트를 생성하며 Spring Initializr에서 필요한 Dependencies들(Web, Thymeleaf, JPA, H2)을 쉽게 추가했었다. 만약 이런 스타터 의존성이 없었다면, 애플리케이션을 개발하기도 전에 build.gradle 또는 pom.xml에서 필요한 Dependencies를 직접 추가해야했을 것이다. (또햔, 여러 의존성들 사이에 잘 호환이 되는지도 확인해야 한다.)

프로젝트의 build.gradle 코드를 잠시 살펴보자.

1
2
3
4
5
6
7
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-thymeleaf')
compile('org.springframework.boot:spring-boot-starter-web')
runtime('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}

Spring Initializr에서 체크했던 의존성들이 gradle에 추가되어 있는것을 볼 수 있다. 또한 각 라이브러리의 버전이 명시되어 있지 않은데, 이는 SpringBoot 버전에 따라 스타터 의존성 버전이 결정되기 때문이다. 즉, 사용자는 스타터 의존성만 지정하면 어떤 라이브러리와 어떤 버전을 사용해야 하는지 걱정없이 구성에서 자유로워질 수 있는 것이다.

자동 구성

SpringBoot Auto-configuration스프링 구성을 적용해야 할지 말지를 결정하는 요인들을 판단하는 런타임 과정이다. 애플리케이션이 시작될 때마다 스프링 부트는 보안, 통합, 데이터 저장, 웹 개발 영역 등을 커버하기 위해 자도성에서 대략 200가지 정도 결정을 내린다. 이 자동 구성 덕분에 필요한 상황이 아니면 명시적으로 구성을 작성하지 않아도 된다.

참고