[Spring] - Lombok을 활용한 이상적인 패턴

회고 과정

멋쟁이 사자처럼 백엔드 스쿨 1기 해커톤을 진행하면서 Spring Boot 를 이용해 협업을 진행하며 알게된 내용을 정리해보고자 한다.

지극히 주관적인 내용이 포함되어 있습니다.

❓ Lombok이란 무엇일까?

필자가 생각하는 Lombok코드 다이어트를 위한 아주 편리한 Library 라고만 생각한다.
즉, 반복되는 메소드를 Annotation을 통해 자동으로 작성해주는 라이브러리이다!

자세한 내용은 아래 코드 설명을 보면서 확인해보자!

💬 Lombok에서 제공하는 유용한 Annotation

Lombok에서 제공하는 몇가지 간단한 어노테이션을 살펴보자 !

@Getter, @Setter

/* -- 작성한 코드 -- */
@Getter @Setter
public class Person{
    private String name;
    private int age;
    private int height;
    private boolean isSolo;
}

/* -- 컴파일 시점 -- */
public class Person{ 
    private String name;
    private int age;
    private int height;
    private boolean isSolo;
		
    public int getName(){return this.name;}
    public int getAge(){return this.age;}
    public int getHeight(){return this.height;}
    public int getIsSolo(){return this.isSolo;}
    
    public int setName(Stirng name){this.name = name;}
    public int setAge(int age){this.age = age;}
    public int setHeight(int height){this.height = nameheight}
    public int setIsSolo(boolean isSolo){this.isSolo = isSolo;}
}

public class Main{
    public static void main(String[] args) {
        Person newPerson = new Person();
        System.out.println(newPerson.getName());
    }
}

앞서 말했던 내용과 같이 @Getter, @Setter만 작성해주면 항상 귀찮게 작성했던
getField(), setField() 메소드들을 자동으로 생성해주면서 코드 다이어트가 가능해진다.

💬 @ArgsConstructor 3형제

@AllArgsConstructor ➡️ 모든 필드를 인자로 받는 생성자를 만듦
@NoArgsConstructor ➡️ 아무 인자가 없는 기본 생성자를 만듦
@RequiredArgsConstructor ➡️ final 혹은 @NonNull이 붙은 필드만 인자로 받는 생성자를 만듦

@AllArgsConstructor, @NoArgsConstructor

/* -- 작성한 코드 -- */
@AllArgsConstructor
@NoArgsConstructor
public class Person{
    private String name;
    private int age;
}

/* -- 컴파일 시점 -- */
public class Person(){
    private String name;
    private int age;

    // @NoArgsConstructor
    public Person(){}

    // @AllArgsConstructor
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
}

@RequiredArgsConstructor

/* -- 작성한 코드 -- */
@RequiredArgsConstructor
public class Person{
    private final String 성별;

    @NonNull
    private String name;

    private int age;
}

/* -- 컴파일 시점 -- */
public class Person(){
    private final String 성별;

    @NonNull
    private String name;

    private int age;

    // @RequiredArgsConstructor
    public Person(String 성별, @NonNull String name){
        if(name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else {
            this.성별 = 성별;
            this.name = name;
        }
    }
}

위에 작성한 내용과 같이 클래스에 있는 Field들을 이용해 자동으로 생성자를 만들어주는 역할을 한다.

🤔 그럼 앞으로 어떤 전략으로 사용해야할까?

@Setter 사용 자제

@Setter 대신 수정 가능한 필드를 대상으로 의미있는 메소드를 사용하자!

@Entity @Getter
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    // 로그인 아이디
    @Column(unique = true)
    private String username;

    private String password;

    private String introduce;

    @Column(unique = true)
    private String email;
    
    public void changeAccountBasicInfo(Stirng password, String introduce){
        this.password = password;
        this.introduce = introduce;
    }
}

위 코드와 같이 Setter 대신 수정 가능부분에 대한 의미있는 메소드를 생성하여 사용!

@AllArgsConstructor 사용 제한 지정

다른 패키지 및 클래스에서 사용을 제한하도록 access를 지정하자!

/* -- 작성한 코드 -- */
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    // 로그인 아이디
    @Column(unique = true)
    private String username;

    private String password;

    private String introduce;

    @Column(unique = true)
    private String email;
}

/* -- 컴파일 시점 -- */
public class Account {
    private Long id;

    private String username;

    private String password;

    private String introduce;

    private String email;
    
    protected Account(){}

    private Account(Long id, String username, int password, int introduce, String email){
        this.id = id;
        this.username = username;
        this.password = password;
        this.introduce = introduce;
        this.email = email;
    }
}

위와 같이 AccessLevel을 사용하면 생성자를 접근 제어자를 protected, private 등으로 지정이 가능하다.
즉, 현재 패키지, 혹은 현재 클래스 이외에서는 해당 생성자를 만들지 못하도록 막는 것이다.

객체를 생성할 때는 @Builder를 사용하자!

public class AccountService{
    public void createNewAccount() {
        // 방식1
        Account newAccount = Account.builder()
                .username(...)
                .password(...)
                .introduce(...)
                .email(...)
                .build();
        Account createAccount = accountRepository.save(newAccount);

// 방식2 - 인라인
        Account createAccount = accountRepository.save(Account.builder()
                .username(...)
                .password(...)
                .introduce(...)
                .email(...)
                .build());
    }
}

위에서 설명한대로 진행하면 앞으로는 객체 생성을 할 때 Builder를 이용해서만 객체를 생성할 수 있을 것이다. 이와 같은 패턴으로 작성하면 해당 객체를 새로 생성한 사람이 어떤 의도로 생성했는지 분명히 파악할 수 있다.

마무리

  1. @AllArgsConstructor는 오직 @Builder를 위해 사용하도록 하며, 다른 곳에서 사용하지 못하도록 AccessLevel.PRIVATE로 지정해주고, 객체를 새로 생성할 때는 @Builder를 이용해 만들어준다.
  2. 객체를 수정할 때는 수정 가능필드들만 속해있으며, 의미있는 메소드를 만들어 계속 재사용한다.
@Entity @Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post{
		private Long id;
		private String title;
		private String content;
		private LocalDateTime createPostTime;
		private List<Reply> replies = new ArrayList<>();

		public void updatePostTitleAndContent(String title, String content){
				this.title = title;
				this.content = content;
		}
}
// Controller는 Repository를 몰라야하기 때문에 Service 단에서 로직을 처리
public class PostService{
		public void createNewPost(PostDto postDto){
				Person newPerson = Person.builder()
					.title(postDto.getTitle())
					.content(postDto.getContent())
					.createPostTime(LocalDateTime.now())
					.build();
				personRepository.save(newPerson);
		}

		public void updatePost(Post post, PersonDto personDto){
				post.updatePostTitleAndContent(postDto.getTitle(), postDto.getContent());
				// 이미 영속화된 상태기 때문에 save는 하지 않음
		}
}

😀 회고

아직 배우는 단계라 리팩터링을 해야할 부분이 많이 보인다.
그래도 이전에 했던 프로젝트에 비해 성장한 모습이 보이는 것 같아 뿌듯하다.

📚 참고 블로그

@frankle97님 블로그
@kay019님 블로그
erjuer님 블로그
@psjw님 블로그

댓글남기기