이제 본격적으로 시작할 차례. 일단 회원가입 기능부터 만드는 게 우선! 

근데 어떻게 만드는지 모른다. mysql과 연결하긴 했지만 과연 잘 된거지도 모른다.

이럴 땐 일단 따라 만드는 수밖에.


우선 Mysql에 Member테이블부터 만들어줬다. DB기초 수업을 20년도 1학기에 수강한 후(사실 이때도 공부를 거의 안함) DB는 전혀 공부한 적이 없었기 때문에..사실 이 부분도 좀 애먹었다. 그래도 어찌어찌 테이블 만드는 건 성공.

 

그리고 프로젝트로 돌아가 application.properties에 다음 코드를 추가해줬다.

 

spring.jpa.hibernate.ddl-auto=none

 

JPA가 자동으로 테이블을 생성하는 기능을 제공해주는데, none값을 요로코롬 멕이면 이 기능을 끈다고 한다. 지금처럼 mysql에서 이미 만든 테이블을 사용하는 경우 이렇게 한다고 함.

 

※ DDL : Data Definition Language의 약자. 데이터베이스 개체를 생성/삭제/변경하는 역할을 하는 놈. none값 외에도 create등을 줄 수 있는데 주로 none을 쓴다고 한다.

 

 

그러곤 Member클래스, MemberRepository클래스, MemberService클래스들을 만들어줬다.

 

package dokky.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

// Entity : 실제 DB의 테이블과 매칭되는 클래스
@Getter
@Setter
@Entity
public class Member {
    // Id : 해당 테이블의 primary key
    @Id
    @Column(name = "email")
    private String email;

    @Column(name = "password")

    private String password;
    @Column(name = "nickname")
    private String nickname;
}

 

JPA를 쓰려면 실제 DB의 테이블과 매칭되는 클래스에게 @Entity라는 어노테이션을 멕여야 된다고 한다. 이래야 매칭되는 듯함. @Id어노테이션은 PK가 되는 컬럼에 멕이는 거고, @Column은 테이블의 어느 컬럼에 매핑되는지를 쓴다. (필드의 이름과 테이블의 칼럼 이름이 같다면 생략 가능하다고 함. 근데 이 플젝에선 그냥 썼다.)

 

 

public class JpaMemberRepository implements MemberRepository {
    private final EntityManager em;

    public JpaMemberRepository(EntityManager em) {
        this.em = em;
    }

    @Override
    public Member save(Member newMember) {
        em.persist(newMember);
        return newMember;
    }
}

 

김영한 님의 입문강의에서는 MemberRepository란 인터페이스를 만들고 걔의 구현체로 실제 사용할 Repository를 만드는 식으로 하길래 따라해봤다. 회원가입을 먼저 만들거니까 일단 save메서드만 만들었다. 사실 김영한님 입문 강의에서는 중간에 DB를 바꾸는 작업 때문에 레포지토리를 인터페이스로 바꾼 거라고 생각해서 굳이 내가 하는 것도 레포지토리 인터페이스를 만들고 할 필요가 없다곤 생각했는데, 그래도 이런 식으로 만드는게 다형성을 활용하는 거니까 나중에 유지보수에 좋다고 생각해서 그냥 인터페이스를 만들고 구현하는 식으로 했다.

 

입문강의에서는  JPA를 쓰려면 EntityManager를 injection하는 구조로 만들어야 한다고 했었다. 이 놈이 db와의 통신을 내부적으로 처리해준다고 강의에 나왔었다. 그래도 이번 기회에 EntityManager가 뭔지 한 번 찾아봤다.

 

  • Entity객체들(@Entity어노테이션이 멕여진)을 관리하며 이놈들을 실제 DB의 테이블과 매핑하여 데이터를 CRUD해주는 객체
  • 내부에 PersistenceContext라는 논리적 영역을 두어, 이를 통해 Entity들을 관리함
  • 김영한님 강의피셜 data-jpa라이브러리를 땡겨오면 부트가 알아서 EntityManager를 생성해준다고 함..! (EntityManager가 Bean으로 자동으로 등록되는 듯)

 

...음..Entity들을 내부적으로 관리해준다는 것만 여기선 이해하고 넘어갈 수 있을 듯 하다. 

아! 참고로 JPA를 정의한 게 javax.persistence패키지라고 한다.

 

 

@RequiredArgsConstructor
// jpa를 통한 모든 데이터 변경은 트랜잭션 안에서 실행!
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;

    public String join(Member newMember) {
        memberRepository.save(newMember);
        return newMember.getEmail();
    }
}

 

@Transactional은 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션이 되도록 보장해주는 어노테이션이라고 한다. 김영한님 강의피셜 JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행돼야 한다고 한다. 그래서 저 어노테이션을 붙여줬다.

 

음..모르는게 너무 많다. 이걸 다 하나하나 짚으면서 이해하고 넘어가긴 지금은 무리다. 일단 하면서 나중에 "이게 이거였구나"라고 알아야 되는 영역같다. ㅠ;

 

암튼 이제 Config클래스를 만들어서 이 놈들을 Bean으로 등록해줬다.

 

@Configuration
public class SpringConfig {
    private final EntityManager em;

    @Autowired
    public SpringConfig(EntityManager em) {
        this.em = em;
    }

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

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

 

근데 갑자기 의문이 생겼다.

 

"왜 Member클래스는 Bean으로 등록하지 않지?"

 

강의를 통해 내가 이해한 것은, 스프링 프로젝트에서 작성한 클래스들을 스프링에서 쓸려면 빈으로 반드시 등록해야 한다는 것이었기 때문이다. 근데 강의를 돌아봐도, Member클래스는 Bean으로 등록을 안 했다.

 

빈으로 등록되는 애들은 싱글턴으로 등록되기 때문에 Member를 Bean으로 하면 안 되는 건 이해되지만, 내가 알고 있던 전제인 "스프링에서 쓸려면 빈으로 반드시 등록해야 함"이 잘못됐다는 걸 깨달았다.  일단 지금은 스프링 컨테이너에 의해 만들어지는 객체 = 빈 이 정도만 이해하고 넘어가야 할 것 같다. 

 

 

이후엔 간단하게 회원가입을 위한 폼을 띄우는 페이지 만들어주고, 회원가입 버튼을 누르면 /account/signup으로 POST요청을 보내게끔 만들었다.

 

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>회원가입</title>
</head>
<body>
    <div>
        <form action="account/signup" method="POST">
            <div>
                <label for="email">이메일</label>
                <input type="email" name="email" id="email">
            </div>
            <div>
                <label for="password">비밀번호</label>
                <input type="password" name="password" id="password">
            </div>
            <div>
                <label for="nickname">닉네임</label>
                <input type="text" name="nickname" id="nickname">
            </div>
            <button type="submit">회원가입</button>
        </form>
    </div>
</body>
</html>

 

이제 이 요청을 처리하는 메서드를 컨트롤러에 작성하면 되는데, 여기서 또 의문이 생겼다. 강의내용에서는 따로 MemberForm이란 클래스를 만들고 이걸 파라미터로 받는 식으로 회원가입 메서드를 만들었다. 근데 MemberForm클래스는 Member클래스와 필드가 똑같고 단순히 Getter, Setter만 있다.

 

    @PostMapping("/signup")
    public String signup(MemberForm memberForm) {
        Member newMember = new Member();
        newMember.setEmail(memberForm.getEmail());
        newMember.setPassword(memberForm.getPassword());
        newMember.setNickname(memberForm.getNickname());
        memberService.join(newMember);
        return "success";
    }

 

대충 이런 식으로 흐름이 진행되던데..어차피 MemberForm이랑 Member랑 필드가 똑같으면 파라미터를 MemberForm이 아니라 Member로 해도 괜찮지 않나?란 생각이 들었다. 이건 그냥 바로 실험으로 옮겨봤다. 되나 안되나..

 

아래 코드로 바꾸고 실험해봤다. 두구두구두구..

 

    @PostMapping("/signup")
    public String signup(Member newMember) {
        memberService.join(newMember);
        return "success";
    }

 

 

 

? 결과는 성공이었다. 뭐지 그럼 왜 강의에선 Member가 아니라 MemberForm으로 받던거지..했는데 이 글을 쓰다가 그걸 알게 됐다.

 

실제 멤버클래스가 가지는 필드 중에 회원가입 폼에서 작성하지 않는 필드가 있을 수 있기 때문이다! 내가 만드는 이 플젝에서는 멤버클래스의 필드 모두를 온전히 멤버폼이 가지고 있으니까 상관없지만, 강의에 나온 예제에서는 멤버가 가지는 id필드값이 폼에서는 작성하지 않는 필드였으니까 멤버폼을 파라미터로 쓰던 것. 

 

오키..근데 아직도 의문인 건 MemberForm은 왜 빈으로 등록 안 하는건지 모르겠다. 으음..스프링이 알아서 MemberForm으로 바인딩해주고 이걸 컨트롤러에 인자로 넘겨주는거면, 내가 이해한 바로는 MemberForm이 빈으로 등록돼있어야 한다. (그래야만 스프링이 관리를 해주는 거고, 관리를 해준다는 것은 그 빈을 꺼내서 주입해준다 등으로 이해했음)

 

모르는 것 투성이다..

 


MemberForm에 대해 더 알아봤다. 저렇게 Client ~ Controller 이런 식으로 각 계층 간에서 단순 데이터 전달을 위한 목적으로 사용되는 객체를 DTO(Data Transfer Object)라고 부른다고 한다. 한 마디로 각 계층끼리 주고받는 편지의 느낌. 로직을 갖고 있지 않은 순수한 데이터 객체로 메서드로는 getter/setter 만을 가진다고 한다.

 

이제 내가 헷갈렸던 거는 멤버폼(DTO)이 아니라 멤버클래스(Entity)를 써서 해도 되던데 왜 멤버폼을 쓰지? 였다. 물론 내가 위에서 기술한 이유인 "실제 멤버클래스가 가지는 필드 중에 회원가입 폼에서 작성하지 않는 필드가 있을 수 있기 때문"도 맞지만, 더 나아가 역할의 분리를 위해서 DTO와 Entity로 구분하는 것이기도 하다. Entity클래스는 단순히 데이터를 담는 것을 넘어 실제 DB와 관련된 중요한 역할을 하는 반면 DTO는 목적 자체가 '전달'이라 그냥 일회성으로만 쓰이기 때문에, 이런 역할 분리를 위해 MemberForm과 Member로 분리해서 쓰던 것. 또한 DTO와 Entity의 생명주기가 다르다는 이유도 있다고 함.

학교에서 하는 스프링 스터디에 참가했는데, 스터디를 위한 공부도 할겸 내 개인 프로젝트도 할겸 OKKY사이트를 모방한 DOKKY라는 서비스를 한 번 이번 겨울방학동안 만들어볼라 한다. 자바 공부도 시작한지 정말 별로 안 됐고 스프링은 이번에 완전 처음 접하기 때문에 완전 야생학습이 됐다..현재 내 스프링에 대한 지식수준은 김영한 님의 스프링 입문 강의(무료)가 전부이다.


 

현재 참여중인 스터디에서 Spring security, Lombok 이런 것들을 사용해서 일단 걔네들도 같이 끌고와서 프로젝트를 생성했다. 그리고 냅다 main메서드를 구동시켰다.

 

 

localhost:8080에 접속하니까 저런 페이지가 나왔다.

음? 나는 웰컴페이지같은 걸 만든 적이 없는데? 뭐지? 싶었다. 스프링이 원래 이런 걸 띄워주는건 아닌거 같고..뭔지 몰라도 security같은 놈이 띄워주는거구나 싶거니 하고 일단 넘어갔다.

 

이번 스터디에서 인증/인가는 Spring security와JWT로 구현하라고 지정돼있었다. jwt는 프론트 공부할 때 사용한 적이 있어서 그리 낯설지만은 않았지만 문제는 시큐리티라는 놈. 스프링에서 인증/인가를 쉽게 해주는 프레임워크다..라는 내용은 구글링해서 쉽게 찾을 수 있었지만 애당초 스프링을 이제 막 떼기 시작한 내게는 이 놈이 너무 낯설었다(사실 글을 작성하는 지금도 이 놈이 정확히 뭔지 왜 쓰는지 잘 모름). 사실 백엔드 단에 대한 지식이 워낙 바닥에 가까워서 그런 것 같기도.. 암튼 구글링을 통해 시큐리티와 jwt를 활용한 로그인 구현 등을 봤지만 도통 무슨 소리들인지 알 수 없었다. 자바, 스프링, 객체지향, 인증/인가 등 다 너무 낯설었고, 낯선 것들이 여기저기 얽혀 있으니 커다란 블랙박스에 마주친 것 같았다.

 

일단 만들면서 배우는 야생형 공부가 효율적이란 건 겪어봐서 알지만, 생판 정말 1도 모르는 상태에서 무작정 해본 적은 없었다.  근데 지금은 그래야 한다! ㅋㅋㅋㅋㅋ

 

이론 공부를 하고 만들기엔 시간이 너무 부족하다. 정말 야생으로 만들면서 배우는게 낫다 싶었다. 우선 DB연결부터 시작했다.

 

application.properties

 

저 파일 gitignore통해서 git으로 관리 안 되게 하는 것에서도 조금 애먹었다. 별 거 아닌데 자꾸 안 됐다. 그래도 해내서 뿌듯했음(정말 별 거 아니지만..)

 

암튼 이제 로그인 기능 만들어볼 차례. post요청을 통해서 /account/login으로 이메일이랑 비번 보내면 처리하게 해야지~라고 생각했다. 김영한 님 입문 강의에서 봤던 방식은 컨트롤러 클래스의 각 메서드마다 @GetMapping같은 어노테이션 멕인 다음에 value로 "/account/login"을 주는 방식이었는데, 클래스에 @RequestMapping을 멕이면 공통된 url을 뺄 수 있었다. 그래서 클래스에 저 어노테이션을 통해 /account로 들어오는 요청들은 요 컨트롤러가 처리하도록 만들었다.

 

@RestController
@RequestMapping(value = "/account")
public class AccountController {
    ...
}

 

아 그리고 또 하나 이 과정에서 알게 된 건 RestController라는 어노테이션이다. 기존에 내가 알던 스프링으로 api만드는 방식은 @Controller 어노테이션과 @ResponseBody 어노테이션을 조합해 활용하는 방식이었다. @ResponseBody를 사용하면 뷰리졸버를 사용하지 않고 HTTP의 BODY에 직접 데이터를 넣는다고 배웠기 때문! 근데 RestController라는 이 둘을 합친 어노테이션이 따로 있었다. 앞으로 되게 많이 쓸 것 같은 느낌.

 

ok. 본격적인 회원가입/로그인 메서드를 만들기 전에, 일단 /account/login으로 GET요청 보내면 간단하게 hola라는 문자열을 리턴하는 메서드를 만들어 시험해보기로 했다. 스프링 생초짜인 만큼 일단 아주 기본적인 거라도 내 예상대로 돌아가는 걸 보고 싶었기 때문. 

 

@RestController
@RequestMapping(value = "/account")
public class AccountController {
    @GetMapping("/login")
    public String login() {
        return "hola!";
    }
}

 

포스트맨켜고, localhost:8080/account/login으로 GET요청을 보내봤다. 너무나도 당연하게 hola!라는 문자열이 돌아오겠거니 했는데..

 

 

아무 일도 없었다.

 

 

뭐지 싶었다. 필시 포스트맨에 문제가 있었으리라 추측하고, 브라우저에 직접 url을 쳤다.

 

 

아닛? 이 창에서 넘어가질 않았다. 뭐지? 아 이거 저 url로 아예 고정된 거 같은데? 란 생각이 들었다..

구글링을 해보니 역시나였다. 스프링 시큐리티를 사용하면 기본적으로 제공되는 녀석이었다. 또한 이걸 끄는 방법 역시 있었다.

 

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
public class DokkyApplication {
	public static void main(String[] args) {
		SpringApplication.run(DokkyApplication.class, args);
	}
}

 

요로코롬 exclude값 설정을 해주면 됐다. 근데 뭔가 시큐리티 자체를 아예 끄는 느낌(이 녀석이 이렇게 끈다 킨다의 개념이 있는 애인지도 모르겠다)..이 들긴 하지만 지금 당장은 그런 걸 다 알아보고 할 순 없다! 진짜 그냥 그때그때 부딪히는 수밖에..

 

암튼 이렇게 하고 다시 포스트맨으로 localhost:8080/account/login으로 GET요청을 보내봤다!

 

 

hola라는 메시지가 response로 돌아오는 것 확인.

이제 본격적으로 회원가입을 만들어볼 차례다.

https://www.acmicpc.net/problem/2075

 

2075번: N번째 큰 수

첫째 줄에 N(1 ≤ N ≤ 1,500)이 주어진다. 다음 N개의 줄에는 각 줄마다 N개의 수가 주어진다. 표에 적힌 수는 -10억보다 크거나 같고, 10억보다 작거나 같은 정수이다.

www.acmicpc.net


계속 고민한 문제다. 그러다 떠오른 방법이 있는데 괜찮은 방법같았다. 우선 각 열들(column)들의 스택으로 갖게 한다. 그럼 스택의 top은 그 열에서 가장 큰 값들을 갖고 있게 된다. 그리고 각 스택의 top들을 비교해 가장 큰 값을 가진 스택에서 pop을 한다. 이 과정을 N번 반복한다. 그러면 N번째에 pop된게 답이다! 

 

시간복잡도는 O(N^2)으로 끊어진다. 우선 입력된 값들로부터 스택들을 세팅하는데 O(N^2)이다. 그리고 N번에 걸쳐 pop을 하는데, N개의 스택들에 대해 매번 top들의 값을 비교하니까 이것도 O(N^2)이다. 따라서 전체 시간복잡도는 O(N^2).

 

이 논리로 문제를 풀었는데, 틀려버렸다. 메모리제한이 있었다.

 

나 고민 많이 했었는데..

 

N x N 보드의 전체 값들을 저장하면 안된다. 아니 그럼 어케 하징?

 

이건 내가 지금 떠올릴 수 없는 쌈박한 접근방법이 필요하다고 느꼈다. 뭐 그 전에도 잘한 건 아니었지만 알고리즘 문제들에서 눈을 돌린지 너무 시간이 많이 지나서..이건 내가 지금 머리끄댕이 잡고 낑낑대는건 시간낭비라고 생각했다. 마에스트로.. 준비해야 되니까!

 

일단 코드는 다음과 같다.

 

원래는 코드에디터로 코드 쓰는데 요즘 새로 쓰는 폰트가 넘 귀여워서 걍 캡처본으로 대체

 

우선 이 문제는 다음 논리로 해결가능하다.

 

  • N개의 값을 보관가능한 박스를 만든다.
  • 모든 숫자들을 박스에 넣어볼 것이다.
  • 박스에 들어있는 숫자가 N개가 아니면 일단 집어넣는다.
  • 박스에 들어있는 숫자가 N개면 박스에 들어있는 숫자 중 제일 작은 값과 지금 넣으려는 숫자를 비교한다
  • 지금 넣으려는 숫자가 더 크면 박스에 그 숫자를 넣고, 박스에서 젤 작은 숫자를 뺀다.
  • 즉 항상 박스안의 숫자는 N개로 유지되며, 모든 숫자에 대한 작업을 끝내면 가장 큰 N개의 숫자가 들어있다.

 

다만 박스를 그냥 리스트로 만들면 시간복잡도가 너무 커진다. 리스트에서 최솟값 찾는 시간복잡도 = O(N)이고 N^2개들의 숫자에 대해서 작업하니까 전체시간복잡도가 O(N^3)이 됨!

 

이 떄 우선순위 큐를 쓰면 쉽게 해결가능해진다.우선순위 큐! 어떤 목록으로부터 항상 작은 값들을 가져오고 싶거나 항상 큰 값을 가져오고 싶거나..뭐 그럴 때 쓰던 놈이다. push, pop모두 log n 만큼의 시간이 걸린다! 따라서 이 문제를 해결 가능.

 

고정된 사이즈의 자료구조를 만들고 활용하는 테크닉.. 꼭 기억하자.

스터디를 진행하던 중 멘토님이 Lombok라이브러리를 사용해서 스터디를 진행하셨다. 하지만 스프링에 막 발을 들인 나는 롬복이 뭔지 모른다. 그래서 정리해봤다.


롬복?

어노테이션 기반으로 코드를 자동완성해주는 라이브러리라고 한다. 개발자 편의를 위해 쓰는 라이브러리인 듯 하다. 다른 언어도 마찬가지지만 자바 언어 역시 기계적으로 작성해야 하는 코드들이 상당히 많이 생기는데 그런 부분들을 자동화해주는 라이브러뤼. 사용한다면 귀찮은 부분들을 작성해주는 걸 편하게 할 수 있을 뿐더러 코드의 길이 자체가 줄어드는 효과를 얻는다는 이점이 있다.

 

 

활용예시 - Getter, Setter

클래스를 만들었다. 이 놈이 갖는 필드 하나하나에 대해 getter, setter를 하나하나 만들어줘야 하는데 여간 귀찮은 게 아니다. 이럴 때 롬복을 이용해 Getter, Setter어노테이션을 주면 지가 알아서 필드들에 대한 getter, setter들을 만들어준다.

 

@Getter
@Setter
public class Member {
    private Long id;
    private String name;
}

 

참고로 클래스 이름 위에 이 어노테이션을 작성하면 모든 필드들에 대해 적용되고, 필드 이름 위에 작성하면 해당 필드에만 적용된다.

 

 

활용예시 - NoArgsConstructor

빈 기본 생성자를 만들어준다.

 

@NoArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member() {
   }
   */
}

 

 

활용예시 - AllArgsConstructor

모든 필드에 대한 생성자를 만들어준다. (DI(Dependency Injection)를 해주는 식으로)

 

@AllArgsConstructor
public class Member {
    private Long id;
    private String name;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

활용예시 - RequiredArgsConstructor

특정 필드들에 대한 생성자를 만들어준다. 도대체 어떤 필드들에 대해 해주냐? final이 붙은 필드들, 그리고 @NonNull 어노테이션이 붙은 필드들에 대해서 자동으로 생성자를 만들어준다. AllArgsConstructor와 마찬가지로 DI(Dependency Injection)을 해주는 생성자를 만들어준다. 

 

※ NonNull 어노테이션 : 롬복에서 쓰는 어노테이션으로, 얘가 멕여진 필드가 null이 되면 NullPointerException을 일으킨다.

 

@RequiredArgsConstructor
public class Member {
    private final Long id;
    @NonNull
    private String name;
    private int age;
    
   /* 이걸 자동으로 만들어준다
   public Member(Long id, String name) {
        this.id = id;
        this.name = name;
   }
   */
}

 

 

이 외에도 어노테이션들이 많다. 클래스에 대한 equals함수와 hashCode함수를 자동으로 만들어주는 @EqualsAndHashCode, 필드들을 기반으로 ToString메서드를 자동으로 만들어주는 @ToString, 객체 생성에 Builder패턴을 적용해주는 @Builder.. 암튼 다양하다. 그때그때 찾아가며 공부하면 될 듯.


스프링에서 @RequiredArgsConstructor를 사용한 생성자 주입

기존에 내가 알고 있던(물론 스프링 공부 시작한지 별로 안됨) 생성자 주입 방식은 이거였다.

 

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

(참고로 memberRepository같은 빈들은 스프링 컨테이너가 관리해준다고 하는데 얘네가 싱글톤 객체라..변하지 않기 때문에 final키워드를 붙이는 게 좋다고 한다)

 

여기서 잠깐 살펴볼 것이, 스프링은 생성자가 1개뿐이라면 @Autowired를 생략해도 된다. 이 경우 @Autowired를 자동으로 인식해 처리하기 때문. 즉 위 코드에서 @Autowired를 빼도 문제가 없다. 

 

그리고 @RequiredArgsConstructor는 final이 붙은 필드들에 대한 생성자를 알아서 만들어준다! 따라서 위 코드를 다음과 같이 아주 편하게 간소화시킬 수 있다.

 

@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
}

 

원리는 간단하다.

 

  • RequiredArgsConstructor가 생성자를 만들어줌
  • 그 생성자에 대한 Autowired를 자동으로 인식해 처리함

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문법을 쓰는게 좋을 것이다.

 

 

 

+ Recent posts