이때까지 Java 및 Spring을 이용하여 개발을 진행할 때, 빌드 도구로써 gradle을 많이 사용하였습니다.
gradle의 좋은 점은 상대적으로 패키지 관리 시에 간결하게 정의된다는 점과 Groovy라는 언어를 사용하기 때문에 자바 개발자들에게는 이해하기 쉽다는 점이 있었습니다.
하지만 현재에도 많은 Spring 프로젝트들은 Gradle 보다는 Maven으로 관리가 되고 있습니다.
필자가 새롭게 공부를 시작한 Spring Boot의 교재에서도 Maven을 사용하고 있기도 하고요.
그래서 Maven으로 프로젝트를 구성하면서 새롭게 학습해보기로 하였습니다.
*.jar . 빌드 산출물에 접근하여 실행하기
교재에서는 Intellij에서 바로 결과물을 실행할 수 있는 기능보다는 *.jar라는 실행 가능한 Java 클래스 파일 모음 및 meta 데이터 및 리소스 파일 모음을 통하여 실행하도록 하였습니다. gradle을 통해 터미널에서 빌드하는 것과는 또 다른 실행 방법이 필요하였습니다. 먼저, 인텔리제이에서 빌드 결과물을 가져오는 설정은 다음과 같습니다.
단위 테스트는 타깃이 되는 객체만을 철저히 검증하는 테스트라고 볼 수가 있습니다. 그 때문에 최대한 의존하고 있는 객체에 영향을 받지 않아야 합니다. 그래서 Mock 객체를 활용하여 의존하고 있는 객체의 행동에 대해서 의도대로 정의를 해주고 단위 테스트를 진행하는 경우가 있습니다. 이때, 특정 행동의 결과로서 메소드에 인자로 받기 전에 해당 값을 중간에 값을 받아 검증하고 싶을 때가 있습니다. 그것을 수행해주는 것이 바로 ArgumentCeptor 입니다.
@Mock이나 @InjectMock을 사용하는 것처럼 테스트 코드에 다음과 같은 어노테이션을 가진 ArgumentCaptor를 추가해주면 됩니다. 이 때, 메소드에서 받는 인자가 List<Integer> 형이기 때문에 저희는 Argument<List<Integer>>를 사용하였습니다.
Exception은 Error와 달리, 구현 로직에서 발생하는 프로그램이 모종의 이유에서 처리할 수 없는 현상을 말합니다. 구현 로직에서 발생하는 경우가 많기 때문에 개발자가 어느 정도 예상이 가능한데요. 하지만 Error와 마찬가지로 프로그램이 돌아가지 않는 것은 같습니다. 때문에 우리는 예외 처리를 통해서 예외의 종류별로 어떻게 처리할 것인지에 대해서 정할 수가 있습니다. 가장 유명한 것은 try - catch 블록이 있습니다.
Spring에서는 조금 더 나아가서 일괄적으로 예외를 처리할 수 기능이 있습니다.
Try - catch를 쓰는 것도 좋지만
먼저, Controller Advice를 사용하지 않았을때의 문제를 살펴 보도록 하겠습니다.
Java를 사용하다 보면 컴퓨터의 구성 요소 중 다양한 부분들을 사양하게 됩니다. JVM에 대한 정보가 있는 메모리부터 데이터베이스 다른 소프트웨어에서 제공 받는 기능들, 그리고 영구적으로 데이터를 할 수 있는 보조 기억 장치까지요. 저희는 이렇게 소프트웨어, 하드웨어적으로 한정된 기능을 제공받는 것들을 자원 이라고 합니다.
자바에서 가장 흔하게 볼 수 있는 자원 관련 객체는 InputStream, OutputStream 그리고 java.sql.Connection 등이 있으며 이 객체들 모두 앞서 말한 요소들에 관한 기능과 연관이 있습니다.
자원 해제의 중요성
컴퓨터의 다양한 기능들을 사용하는 것은 좋지만, 해당 상황들은 우리가 만드는 프로그램 뿐만이 아니라 각각의 자원에 대해서도 영향을 미치게 됩니다.
1. JVM에서 차지하는 메모리
외부 자원을 사용하게 되는 것도 JVM에서 필시 메모리가 차지하게 됩니다. 이는 우리의 프로그램이 GC를 통해서 처음부터 끝까지 관리해주는 객체들과 달리 다른 자원과 연결되어있습니다. 때문에 GC를 믿기보단 직접 해제를 해줘야 합니다.
2. 관련 객체 오작동
자원에서 필요한 부분들을 다 사용하고 나서 반납을 해주지 않으면 다른 프로세스나 쓰레드에서 해당 객체를 사용했을 때의 예상치 못한 문제가 발생 할 수 있습니다.
이러한 이유 때문에 GC를 믿기보다는 해당 자원을 직접적으로 해제해주는 것이 좋습니다.
기존의 방법들
자원에 관련된 객체 이해를 돕기 위해 임의로 하나의 객체를 정의 해보겠습니다.
public Resource {
publicvoidrun()throws ResourceExcpetion {
//해당 자원에 관련된 로직을 실행하는 메소드
}
publicvoidclose()throws ResourceExcpetion {
//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
}
}
가장 원시적으로 사용하게 되는 방법은 자원을 사용하고 난 다음에 일일히 메소드로 닫아주는 형태입니다.
publicclassMain{
publicstaticvoidmain(String[] args){
Resource resource = new Resource(); //자원 할당
resource.run(); //자원 사용
resource.close(); //자원 사용 해제
}
}
굉장히 단순해 보이지만 문제가 있습니다. 만약 run이라는 메서드를 사용하는데, 프로그램 내부의 로직 문제든 외부 자원에서 발생한 문제이든 Exception 이 발생 될 수가 있을 것입니다. 그러면 자원 사용을 해제하는 close()에 대한 메소드는 실행되지 않을 것입니다.
이러한 예상되는 문제점들을 해결하기 위해서 사람들이 활용한 것이 try-catch-finally로 대표되는 자바의 예외 처리 구문을 사용하기로 하였습니다.
publicclassMain{
publicstaticvoidmain(String[] args){
Resource resource = new Resource(); //자원 할당try {
resoure.run(); //자원 사용
} finally {
resource.close(); //자원 사용 해제
}
}
}
해당 예시처럼 try 블록을 사용한 다음에 메소드가 작동 여부에 상관 없이 finally 블록에 진입하여 자원을 닫아주게 됩니다.
하지만 이러한 부분도 문제가 크게 두 가지가 존재합니다.
1. 깊어지는 Depth의 문제
try - finally 방법을 사용하게 된다면, 자원의 수에 맞게 try-finally 블록이 계속해서 발생하게 됩니다. 만약 우리가 사용할 자원이 두개 이상이 되면, 사용하는 자원의 수인 2개 이상만큼 try - finally를 사용하는 것이 이상적입니다.
publicclassMain{
publicstaticvoidmain(String[] args){
Resource resource = new Resource(); //자원 할당
SecondResource secondResource = new SecondResource();
try {
resoure.run(); //자원 사용try {
secondResource.run(); //자원 사용
} finally {
secondResource.close(); //자원 사용 해제
}
} finally {
resource.close(); //자원 사용 해제
}
}
}
하지만 이런 방향은 코드의 Depth를 늘리게 되고, 이는 객체 지향 프로그래밍에 맞지 않은 결과를 초래할 뿐만이 아니라 가독성이 떨어지는 문제점이 생기게 됩니다.
2. 삼켜져 버린 예외 처리
만약 run()이라는 메소드에서 예외가 발생한다면,이 예외가 로그에 기록되는 것이 우리가 바라는 방향입니다. 하지만 try - finally를 사용하게 된다면, run()에서 발생한 예외보단 finally에서 발생한 예외가 기록이 됩니다. 이는 디버깅이나 기능을 구현할 때 예외 처리에 대한 로그 기록이 남지 않으므로 문제가 생길 수 있습니다.
개선된 방법 - try with resource
Try with Resource를 사용한 코드를 먼저 보고 가도록 하겠습니다.
publicclassMain{
publicstaticvoidmain(String[] args){
try(Resource resource = new Resource();){ //자원 할당
resoure.run(); //자원 사용
}
}
}
앞서 보여드렸던 기존의 코드와 다른점은 2가지가 있습니다.
첫 번째로는 try 블록안에 자원 할당을 하는 코드가 들어갔습니다.
두 번째로는 resource를 닫아주는 close() 메소드가 사라지게 되었습니다.
이로 인해서 저희는 Depth에 관한 문제와 예외 처리에 관한 문제를 동시에 해결할 수가 있습니다.
만약 여기서 발생한 예외에 대해서 잡고 싶다면
try 뒤의 블록에
} catch (ResourceException resourceException) {
//예외 처리 로직
}
을 진행해주시면 됩니다.
하지만 이렇게 좋아 보이는 try-with-resource 방법은 자원에 대한 객체면 다 쓸 수 있는 것이 아닙니다.
특정 인터페이스를 구현해주어야하는데 그것이 바로 다음에 언급할 AutoCloseable입니다.
Interface AutoCloseable, Method close
//java.lang의 패키지라 따로 import할 필요없습니다.public Resource implements AutoCloseable {
publicvoidrun()throws ResourceExcpetion {
//해당 자원에 관련된 로직을 실행하는 메소드
}
@Overridepublicvoidclose()throws ResourceExcpetion {
//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
}
}
AutoCloseable의 경우에는 close를 추상 메소드로 가지고 있는 인터페이스 입니다.
이 close를 오버라이딩을 통해서 구현해주면 try-with-resource에서 자원을 자동으로 해제해주는 객체를 만족하게 됩니다.
아까와 다르게 객체 People을 정의하는 과정에서 "implements Comparable <People>"이라는 부분이 추가된 것을 확인할 수가 있습니다.
implements Comparable이라는 부분은 객체에서 인터페이스 Comparable를 구현해주겠다는 것을 의미합니다. 이때, 인터페이스는 실질적인 구현 대신에 명세서처럼 메서드 선언을 가지고 있다는 것을 의미합니다.
인터페이스 Comparable을 implements 한 People은 Comparable이 추상적으로 가지고 있는 compareTo를 자신만의 기준에 맞게 구현하게 됩니다.
여기서 Comparable <People>에서 나오는 괄호 기호 <>은 자바의 기능 중 하나인 Generic으로써, 외부에서 타입을 정할 수 있게 해주는 기능입니다. 이때 저희는 <People>로 지정을 하였으니,Comparable라는 인터페이스 기준으로 한 외부의 관점에서 People이라는 타입을 사용하겠다!라는 것이 됩니다.
//Collcections.sorts(peoples, comparator); //Comparator 사용
peoples.sort(comparator); //List API 사용
Collections.sort()를 바로 List에 사용하던 Comparable과 달리 여기서는 Comparator를 List의 정렬 API인 sort()에 인자로 넣어주거나, Collection.sort에서 마찬 가지로 우리가 원하는 방향으로 정의된 Comparator를 추가해주면 됩니다.