try with resources 문법은 try-catch문에서 선언된 객체들에 대해 try문을 벗어날 때 자동으로 자원을 해제해주는 문법이다. 

 

사실 여기서 글 작성을 완료해도 되지만..예시를 들어야겠죵?


기존에는 다음과 같이 자원을 해제했었다.

 

try {
    is = new FileInputStream("file.txt");
    dis = new DataInputStream(is);
    // ...생략
} catch (IOException e) {
    e.printStackTrace();
} finally {
    dis.close();
}

 

DataInputStream을 이용해 파일로부터 데이터를 읽는 코드로, 데이터를 읽다가 예외가 생겨도 finally블록의 close를 통해 자원을 해제한다. 그러나 사실 close()가 예외를 발생시킬 수 있다는 함정이 숨어 있다. 따라서 finally블록 안에서 다시 한 번 try catch를 해줘야 한다.

 

try {
    is = new FileInputStream("file.txt");
    dis = new DataInputStream(is);
    // ...생략
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        if (dis != null) {
            dis.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

 

그러나 indent가 꽤나 늘어나게 되는 걸 볼 수 있다. 한 눈에 봐도 코드가 더러워진다..좀 더 쉽게 자원을 해제하고 싶은 우리를 위한 바로 그 문법! try-with-resources문법이다. try의 괄호 () 안에 객체를 생성하는 코드를 넣으면, 그 객체는 따로 close()를 호출하지 않아도 try블록을 벗어나는 순간 자동적으로 close()를 호출하게 된다.

 

try (is = new FileInputStream("file.txt");
    dis = new DataInputStream(is)) {
    // ...생략
} catch (IOException e) {
    e.printStackTrace();
}

 

그렇다고 아무 객체나 냅다 반납(해제)가 가능한 건 아니다. try-with-resources문법으로 해제되고 싶은(?) 객체는 AutoCloseable이라는 인터페이스를 구현을 해야한다. 다음 코드가 AutoCloseable인터페이스다.

 

public interface AutoCloseable {
    void close() throws Exception;
}

 


코드의 복잡도를 줄여준다는 점 외에도 자원 해제를 해야 하는 경우 try-with-resources문법을 사용하는 게 좋은 이유는 또 하나 있다. 그건 바로 에러 스택 트레이스가 누락될 수 있다는 점이다. 다음과 같은 클래스가 있다고 해보자.

 

public class CloseableResource implements AutoCloseable {
    public void exceptionWork() throws WorkException {
        System.out.println("exceptionWork()가 호출됨!");
        throw new WorkException("WorkException 발생!!");
    }

    public void close() throws CloseException {
        System.out.println("close()가 호출됨!");
        throw new CloseException("CloseException 발생!!");
    }
}

class WorkException extends Exception {
    WorkException(String message) {
        super(message);
    }
}

class CloseException extends Exception {
    CloseException(String message) {
        super(message);
    }
}

 

그리고 다음과 같은 try-catch-finally문이 있다고 하자.

 

CloseableResource cr = null;
try {
    cr = new CloseableResource();
    cr.exceptionWork();
} finally {
    if (cr != null) {
        cr.close();
    }
}

 

우선 try블록에서 WorkException를 발생시킨다. 그리고 예외의 발생여부에 상관없이 항상 실행되는 finally 블록에서 close를 해주는데, 이 때 CloseException도 발생될 것이다. 그러면 우리의 상식(?)으로는 에러 스택 트레이스에 Work, CloseException이 둘 다 쌓여야 한다. 그러나 결과는..

 

 

보다시피 try블록에서 발생했던 WorkException이 누락된 모습이다. 비단 try블록 뿐만이 아닌 catch블록에서 발생된 예외도 이와 같은 상황에선 누락된다. 이러한 문제가 실무에서 발생됐다면.. 그 원인을 파악하기가 좀처럼 쉽지만은 않을 것이다.

 

그러나 다음과 같이 try-with-resources문으로 작성했다면 어떨까.

 

try (CloseableResource cr = new CloseableResource()) {
    cr.exceptionWork();
}

 

WorkException이 일어나고 try블록을 벗어나면서 close()가 호출되며 CloseException도 일어날 것이다. 결과는..

 

 

에러 트레이스가 누락되지 않은 모습을 볼 수 있다!! 때문에, 자원을 해제하는 경우 try-with-resources문법을 쓰는게 좋을 것이다.

 

 

 

스프링으로 DB의 데이터에 접근하는 방법은 크게 다음과 같이 4가지 방식이 있다.

 

  1. jdbc
  2. jdbcTemplate
  3. JPA
  4. 스프링 데이터 JPA

 

각각이 뭔지 간단하게만 알아보자.


1. Jdbc

Java Database Connectivity의 줄임말로, 자바 언어를 통해 DB와 연동하여 데이터를 주고받을 수 있게 해주는 API이다. 자바 언어로 작성된 응용프로그램과 DB의 중간에서 서로간의 통신을 번역해주는 녀석이라고 생각할 수 있다. 

 

기존의 DB접근 방법은 TCP/IP를 통해 서버와 DB가 서로 커넥션을 연결하고, SQL을 DB에게 전달하면 DB가 전달받은 SQL을 수행하고 그 결과를 응답하는 방식이었는데 문제는 각 DB들마다 저마다의 사용방법과 접근방법이 조금씩 다르단 것이었다! 즉 DB를 바꿀 때마다 그에 맞춰 접근하는 코드도 함께 바꿔야하는 큰 번거로움이 있었고, 이를 해결하고자 DB접근 표준 인터페이스인 Jdbc가 등장한 것이다.

 

그러나 단점으로는 상당히 복잡한 코드를 짜야 돼서 고생을 많이 한다는 거고, sql 쿼리는 직접 짜야 한다. 

 

DB와 커넥션을 맺고, SQL을 담은 내용(Statement)을 보내고, 그에 대한 응답(ResultSet)을 받고, 커넥션을 끊는 4단계로 동작한다.

 

2. JdbcTemplate

이 놈은 실무에서도 많이 쓴다고 한다. 순수 jdbc를 좀 더 효율적으로 사용할 수 있게끔 해주는 틀로, 스프링 프레임워크에서 제공해주는 놈이다. 순수 Jdbc의 단점인 "복잡한 코드 짜야 함!"을 개선하여, Jdbc에서 흔히 마주치는 반복 코드를 대부분 제거한다. 그러나 SQL쿼리는 직접 짜야 한다는 단점이 존재하긴 한다. 요약하자면 jdbc로 좀 더 간결한 코드를 짤 수 있게끔 해준다.

 

3. JPA

Java Persistence API의 줄임말이다. 자바 진영의 ORM기술 표준 인터페이스이고, 이를 여러 업체가 구현체로 만들었으며 대표적으론 hibernate 등이 있다. JdbcTemplate가 해냈던 기존의 반복 코드 제거는 물론 기본적인 SQL도 JPA가 지가 알아서 만들어서 실행해준다. 이를 통해 개발자가 SQL에 초점을 맞추지 않고 객체 중심의 설계에 좀 더 초점을 맞추게 된다는 점에서 개발 생산성이 크게 높아지게 된다.

 

※ ORM : Object Relational Mapping의 약자로 객체와 관계형 DB를 매핑하는 것을 말함

 

4. 스프링 데이터 JPA

스프링에서 jpa를 편하게 쓸 수 있도록 지원해주는 기술이다. save, findAll, delete와 같이 간단한 CRUD기능들을 공통으로 처리하는 JpaRepository를 제공한다. 인터페이스 메서드만 선언하면 메서드 이름만으로 쿼리를 생성하는 마법같은 기능도 제공한다. (예를 들어 findByName으로 메서드를 지으면 지가 알아서 이름으로 찾는 쿼리를 작성해줌. 물론 이를 위해서 당연히 정해진 규칙에 따라 메서드 이름을 작성해야 함. 자동으로 내부에서 구현체를 만들어 동작시키는 것임)  

 

 

 

스프링에서 사용할 객체를 위해 클래스를 만들어줬다고 해도 그 녀석을 사용하려면 스프링 빈으로 등록을 해야 한다. 만약 빈으로 등록하지 않으면, 스프링에서 사용할 수 없게 된다. 근데 빈이란 것이 도대체 무엇일까?

 

스프링 빈(Bean)

스프링 컨테이너가 관리하는 Java객체. 즉 스프링에 의해 생성되고 관리되는 Java 객체라고 보면 된다. 스프링 컨테이너가 일종의 바구니 같은 느낌이라면, 빈은 그 바구니에 담기는 콩들이라고 생각할 수 있다. 한마디로 이 콩으로 등록을 해야 스프링에서 이용가능하게 된다는 것..! 원래는 개발자가 직접 코드를 통해 new연산자나 팩토리 패턴 등으로 객체를 생성하고 소멸시키는데, 스프링은 컨테이너라는 바구니가 이 역할들을 대신해준다고 생각할 수 있다.

 

예를 들어 내가 MemberController를 만들고, 이 컨트롤러가 MemberService를 통해 회원가입이나 회원조회를 할 수 있게끔 만들려고 한다면 내가 만든 MemberController와 MemberService를 빈으로 등록을 해야 스프링에서 이용 가능하다. 이 때 다음과 같은 2가지 방법으로 스프링 빈 등록이 가능하다.

 

1. 어노테이션을 통한 등록

빈으로 등록할 클래스에 @Component라는 어노테이션을 멕이면 된다. 이렇게 해두면 스프링으 컴포넌트 스캔을 통해 지가 알아서 Bean으로 등록해준다. @Controller, @Service, @Repository등과 같이 @Component를 포함하는 어노테이션들도 컴포넌트 스캔을 통해 Bean으로 등록된다. 근데 그렇다고 아무데서나 저런 어노테이션 멕인다고 죄다 Bean으로 등록되는건 아니고, 컴포넌트 스캔을 하는 범위가 따로 있다. 기본적으론 @ComponentScan 어노테이션이 있는 파일의 패키지 아래를 모두 스캔하며, @SpringBootApplication(scanBasePackages ={{경로설정}}) 옵션으로 컴포넌트 스캔 시작 범위를 직접 설정할 수 있다. (이 경우 설정한 경로 기준으로 그 하위 애들만 스캔)  

 

암튼 어노테이션을 통한 등록법의 예시는 다음과 같다.

 

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

@Autowired : 이 어노테이션이 보시다시피 생성자에 있으면 스프링에 컨테이너에 있는 memberService 빈을 찾아서 자동으로 연결해주게 한다(즉 이를 위해서 MemberService가 당연히 빈으로 등록돼있어야 한다). 이렇게 의존관계를 갖는 객체를 외부에서 넣어주는 이런 모습을 DI(Dependency Injection)이라고 한다. 여기서는 개발자가 직접 코드를 통해 주입하는 게 아니라 @Autowired 어노테이션에 의해 스프링에 알아서 주입해준다.

 

2. 자바 코드로 직접 등록

하나하나 직접 Bean으로 등록하는 방식이다. ~~Config라는 자바 파일을 만들고 거기에 다음과 같이 빈을 등록한다.

 

@Configuration
public class SpringConfig {
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

참고로 MemberService가 MemberRepository에 의존하기 때문에 MemberService의 경우 생성자에서 memberRepository를 넣어주는 모습.

 

@Bean이란 어노테이션을 보고 스프링이 이 객체들을 Bean으로 등록해준다. 

 

 

두 방법 중 그런 어떤 방법이 더 좋은 것일까?

 

상황에 따라 다르다.

 

주로 정형화된 컨트롤러나 서비스, 리포지토리 같은 경우는 어노테이션을 통한 등록이 편하다.

그러나 정형화되지 않았거나 상황에 따라 구현 클래스를 변경해야 하는 경우 등엔 코드로 직접 등록하는 게 더 편할 것이다. 왜냐하면 어노테이션을 통한 등록의 경우 변경할 때마다 새로 어노테이션을 작성해줘야 하는 번거로움이 있지만 직접 등록하는 경우는 1번만 바꾸면 되기 때문.

스프링 웹 개발은 다음과 같은 3가지 방법이 있다.

 

  1. 정적 컨텐츠 : 서버에서 특별히 뭔가 하는 거 없이 단순히 html파일을 클라이언트 쪽으로 넘기는 방식이다. 
  2. MVC & 템플릿 엔진 : 서버에서 특정한 작업을 통해 html파일을 동적으로 만들어 넘기는 방식이다. JSP, PHP 등의 템플릿 엔진이 사용되며 이걸 하기 위해 MVC패턴이 도입된다.
  3. API : JSON 포맷 등으로 '데이터'를 클라이언트에 전달하는 방식이다. 서버 간의 통신에도 활용되며, 요즘은 React같은 애들한테 API로 데이터를 넘기고 그걸 통해 화면을 클라이언트가 그리는 식으로도 활용된다.

 

정적 컨텐츠

resources폴더의 static폴더에 있는 파일을 찾아 넘긴다. 다음 순서로 동작한다고 이해할 수 있다.

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 없으면 내부의 resources/static에서 찾아서 넘김

 

MVC & 템플릿 엔진

MVC는 Model, View, Controller로 각자의 역할을 구분해 개발하는 방식을 일컫는다. Model과 Controller는 비즈니스 로직과 내부 로직을 처리하는 역할을 맡고, View는 화면을 그리는 역할을 맡는다. 옛날엔 JSP를 통해서 View에서 Controller의 역할도 수행했는데 이를 model 1방식이라 부르기도 함.

 

@Controller
public class HelloController {
    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!!");
        return "hello";
    }
}

 

  • GetMapping : GET POST 할 때의 get이며, /hello로 get요청이 들어왔을 때 hello라는 메서드를 실행한다는 거다. 
  • Model : View를 그리기 위해 사용되는 데이터들의 박스(?)로, key-value형태로 데이터들을 저장한다. 내부적으론 Map이 사용된다고 함.
  • return "hello" : 데이터들을 보낼 View이름을 지정하는 거라고 보면 된다.

 

 

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. viewResolver가 지정된 이름의 View를 찾아주고 템플릿 엔진과 연결시켜줌
  6. 템플릿엔진이 렌더링한 html파일을 반환

 

API

이 방식은 html파일이 아니라 데이터를 넘기는 방식이라고 볼 수 있다. 결국 정적 컨텐츠를 넘기는게 아니라면

 

  • html로 내려주든가
  • api로 (즉 데이터로) 주든가

 

둘 중에 하나인 거다.

 

@Controller
public class HelloController {
    @GetMapping("hello-string")
    @ResponseBody
    public String helloString(@RequestParam("name") String name) {
        return "hello" + name;
    }
}
  • GetMapping : GET POST 할 때의 get이며, /hello-string로 get요청이 들어왔을 때 helloString라는 메서드를 실행한다는 거다. 
  • @ResponseBody : 이 어노테이션을 쓰면 viewResolver를 쓰지 않고, 대신에 Http Response의 BODY에 데이터를 담게 된다.
  • @RequestParam("name") String name : 쿼리에서 "name"에 해당하는 값을 name이란 파라미터로 받아온다는 거다.
  • return "hello"  + name : 이 문자열 자체를 데이터로 준다는거다.

 

참고로 객체를 반환하면 객체가 JSON으로 변환되서 보내진다!

@Controller
public class HelloController {
    @GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. 근데 @ResponseBody가 있으면 viewResolver에게 전달하지 않고 httpMessageConverter에게 전달함
  6. 문자는 그대로 보내고, 객체는 JSON으로 바꿔서 전달한다.

 

 

용어 정리

Gradle : 라이브러리들을 버전 설정하고 가져와주는 빌드 도구. 은 의존관계가 있는 라이브러리들도 함께 다운해준다(A라이브러리가 B에 의존하면 자동으로 B도 땡겨줌).

 

resources폴더 : 자바 파일(*.java)를 제외한 나머지 파일들을 위한 폴더

 

템플릿 엔진 : 지정된 템플릿 양식과 데이터를 합쳐 HTML 문서를 출력하는 SW. 예를 들어  그냥 a.html을 작성하기만 하면 서버에선 그냥 걔를 클라이언트쪽으로 넘기기만 하는데, 템플릿 엔진을 쓰면 html에서 루프를 쓴다던가 어떤 데이터값을 넣는다든가 할 수 있는 것이다. 

 

<html xmlns:th="http://wwww.thymeleaf.org">

 

html태그에 이렇게 작성해두면 해당 html파일에서 타임리프(템플릿 엔진) 문법을 사용 가능하다. 이를 활용해 서버에서 받아온 특정 데이터 값을 넣고 싶으면

 

<p th:text="'안녕하세요' + ${data}">안녕하세요.손님</p>

 

이런 식으로 하면 된다. 참고로 태그 사이엔 딱히 뭔가를 쓸 필욘 없음. 근데 템플릿 엔진을 거치지 않고 html파일을 열거나 할 때 태그 사이의 값이 나온다. 이미지 태그의 alt값과 비슷한 역할이라고 생각할 수 있겠다.

 

 

스프링 강의를 수강하기 시작하며 Optional이란 걸 마주쳤다! null값으로 올 수 있는 애들을 위해서 실무에서 많이 쓴다던데?

이제 이런 것들 안 지나치고 내 것으로 만들어야겠다는 생각이 든다. 바로 알아봤음.


일단 이 녀석은 Integer클래스처럼 일종의 Wrapper클래스다. 포장해주는 느낌. 그리고 그 알멩이로는 모든 타입의 변수가 저장될 수 있다.

 

내부 알멩이로 null 역시 저장될 수 있기 때문에, null값과 관련된 예상치 못한 NPE(NullPointerException)들을 피할 수 있다는게 가장 큰 장점이다. 예를 들면 어떤 저장소에서 find를 했는데 내가 찾는 게 없어서 null값이 반환된 경우 등..이런 상황을 방지하려면 find한 결과가 null인지 아닌지를 if문으로 파악해야 했지만 Optional을 사용하면 그런 걸 간단히 처리할 수 있다는 게 장점쓰. 애당초 null일 수도 있는 값을 다루기 위한 클래스이기 때문에 그와 관련된 다양한 메서드들을 제공한다.

 

그러면 일단, 이 Optional 객체를 어떻게 만드는가? Optional클래스를 뜯어보면 다음과 같이 of와 ofNullable이라는 스태틱 메서드가 정의돼있는 것을 볼 수 있다.

 

 

두 메서드 모두 Optional을 리턴하는 걸 볼 수 있다. 즉 이 두 메서드를 통해서 Optional객체를 생성할 수 있다! of메서드는 인자로 'null이 아닌 값'을 받고, ofNullable메서드는 인자로 'null값일 수도 있는 값'을 받는다.

 

of메서드의 경우 인자로 준 값이 null이면 NPE예외가 발생한다. 또한, ofNullable메서드의 경우 인자로 준 값이 null이면 EMPTY, 즉 빈 Optional객체가 리턴된다. 아니 그냥 내부에 null값 넣어서 리턴해주지 왜 약속된 빈 Optional객체(= EMPTY)를 리턴하도록 했을까? 답은 메모리 절약을 위해서. 어차피 알멩이가 null인 Optional 객체는 솔직히 의미없는 객체인데, 여러 곳에서 이 깡깡이 Optional객체를 만들었다면 메모리 낭비가 있을 것이다. 이를 위해 Optional클래스가 싱글턴 인스턴스로 EMPTY 객체를 가지는거고, 얘 하나만을 빈 객체로 리턴해주는 것이다. 개발자 분들이 메모리 절약을 위한 센스있는 선택을 하신 것 같다. 캬..

 

 

그럼 이 Optional객체가 감싸고 있는 알멩이는 어떻게 꺼낼 수 있을까? 간단하다. get메서드로 꺼낼 수 있다. 근데 음..이렇게 꺼내쓰는 건 권장되지 않는다고 한다. 만약 Optional객체가 감싸고 있는 값이 null이면 NoSuchElementException예외가 발생하기 때문. 즉 다짜고짜 꺼내는 게 아니라 안에 있는 알멩이가 null인지를 확인하고 꺼내는 게 좋다. isPresent메서드가 이 기능을 해준다.

 

참고로 isEmpty메서드는 는 반대로 알멩이가 null이면 true를 리턴한다

 

그럼 만약 알멩이가 null인지를 확인하고, null이 아니면 바로 뭔가 해주고 싶으면? ifPresent메서드를 사용할 수도 있다. 이 메서드는 알멩이가 null이 아니면 파라미터로 받은 action을 실행해준다.

 

뭐 람다식같은 거 인자로 주면 되겠죵?

 

만약, 알멩이를 꺼내고 싶은데 알멩이가 null이면 내가 지정한 값을 쓰고 싶다면? orElse메서드를 쓰면 된다.

 

 

 

이외에도 여러 메서드들이 있다. 실무에서는 null값이 될 수도 있는 애들을 Optional로 래핑해서 처리하는 경우가 많다고 한다. 나도 이 방법을 숙지하도록 하자. 아자아자..!

 

※ 참고 : orElse vs orElseGet에 관련된 좋은 글

https://ysjune.github.io/posts/java/orelsenorelseget/

 

orElse 와 orElseGet 무슨 차이가 있을까?

The Lazy Snack's Cargo

ysjune.github.io

 

+ Recent posts