본문 바로가기

Trouble Shooting/Spring

Effective JAVA를 Spring 프로젝트에 적용해보자 (Item 1~5)

 

 

 

Item1 : 생성자보다 정적 팩터리 메서드 사용을 고려하자.

기본적으로 spring에서 클래스에 대한 인스턴스 생성경우는 여러가지가 존재한다.

- Controller, Service, DAO 등 기본적인 베이스 클래스에 대한 생성

=> Spring에서는 해당 클래스의 인스턴스 생성을 관리해준다. 따라서 인스턴스 생성에 대해 많은 부분을 고려할 필요가 없다.

- DTO, Entity 등 클래스

=> 클라이언트에 요청에 맞게 생성하는 경우에는 사용이 불필요하다.

(통신에서 기본생성자와 getter/setter를 통해 인스턴슬 생성하기에 생성자 생략이 어렵다.)

=> 개발자가 프로그램 중간에 임의로 인스턴스를 생성하는 경우에 사용한다.

 

 

 

적용 사례

MemberDTO : 회원가입 관련 DTO

public class MemberDTO {

  private static final MemberDTO sample = MemberDTO.builder().userId("id").userPwd("pwd")
      .userName("name").userNumber("number").userAddress("address").userEmail("email").build();

  @NotNull
  private String userId;
  @NotNull
  private String userPwd;
  @NotNull
  private String userName;
  @NotNull
  private String userNumber;
  @NotNull
  private String userAddress;
  @NotNull
  private String userEmail;

  public static MemberDTO sample() {
    return sample;
  }
}

 

 

 

장점을 적용한 부분

 

1. 이름을 가질 수 있어서 용도에 대한 이해도가 높아진다.

- sample 메서드는 이름 그대로 sample을 반환해준다.

 

2. 인스턴스 낭비를 막는다.

- sample을 프로그램 시작시 미리 생성해두고 이를 반환하여 인스턴스 생성의 낭비를 막는다.

@Test
@DisplayName("MemberDTO : sample Test : 같은 인스턴스를 사용하는 가?")
void MemberDTOSample() throws Exception {
    MemberDTO sample1 = MemberDTO.sample();
    MemberDTO sample2 = MemberDTO.sample();
    assertTrue(sample1 == sample2);
    assertTrue(sample1.getUserId() == sample2.getUserId());
}

 

그 밖의 여러 장점들이 존재하지만 기본적으로는 객체 생성이 클라이언트 요청에 따른 객체 생성이고 이는 기본 생성자와 getter/setter 메서드를 통해 생성되므로 적용사례가 흔하지는 않다.

 

 

 

 

Item 2 : 생성자에 매개변수가 많다면 빌더를 고려하라.

 

 

적용 사례

Spring으로 서버를 만들다보면, 기본적으로 매개변수가 많은 경우가 많다.

아래와 같이 엔터티의 생성자에 DTO를 그대로 받아 BeatifulUtils를 사용해 바로 변환도 가능하지만, 기본적으로 생성자에 매개변수를 받아 각각 설정해 인스턴스를 생성한다. 하지만, 이러한 경우 직관적이지 않고 매개변수가 많아지면 의미를 파악하기 어렵다.

따라서, 빌더로 점층적 생성자 패턴의 문제를 해소해 인스턴스를 좀 더 직관적으로 생성하고 자바빈즈 패턴의 문제인 인스턴스 변화 가능성도 배제시켰다.

 

하지만, 기본적으로 변화가능성을 배제시킨것은 모순이다. 예를들어, DB에서 데이터를 불러와 엔터티를 만드는 경우 기본생성자로 인스턴스를 생성하고 getter/setter로 해당 값을 넣는다. 따라서, getter/setter는 존재하므로, 빌더로 생성후 이를 활용해 변경이 가능하다.

따라서, 책에서 나오는 불변 객체 생성 부분에 대한 적용은 현재로서는 프로젝트에서는 어렵다. 하지만, 활용 가능한 부분이 분명 존재한다.

 

public class MemberEntity extends BaseEntity {

  private static final MemberDTO sample =
      MemberDTO.builder().userId("userId").userPwd("userPwd").userName("userName")
          .userNumber("userNumber").userAddress("userAddress").userEmail("userEmail").build();

  @Id
  private String userId;
  private String userPwd;
  private String userName;
  private String userNumber;
  private String userAddress;
  private String userEmail;


  public MemberEntity(MemberDTO member) {
    BeanUtils.copyProperties(member, this);
  }

  public static MemberDTO toMemberDTO(MemberEntity memberEntity) {
    return MemberDTO.builder().userId(memberEntity.userId).userPwd(memberEntity.userPwd)
        .userName(memberEntity.userName).userNumber(memberEntity.userNumber)
        .userAddress(memberEntity.userAddress).userEmail(memberEntity.userEmail).build();
  }

  public static MemberDTO sample() {
    return sample;
  }
}

 

 

Item 3 : private 타입이나 열거타입으로 싱글턴임을 보증하라.

현재, 프로젝트에서는 적용사례가 없었다.

하지만, 프로젝트 진행 중에 하나의 인스턴스를 만들고 이것만을 활용해, 적용할 수 있는 사례가 많다.

 

적용 예시

예를 들어, 프로그램에 여러가지 게시판이 존재하고 게시판에 게시글을 올릴때마다 적용되는 규칙이 같을 수 있다.

이러한 경우, 규칙을 검사해주는 클래스를 만들고 하나의 인스턴스만을 사용해 이를 검사할 수 있다.

 

 

싱글턴임을 보증하는 방법

 

1. private static final 인스턴스 + private 생성자 + 정적 팩터리 메서드로 인스턴스 전달

 

장점

1. API를 변경하지 않고도 싱글턴이 아니게 변경가능하다.

-> 기존의 인스턴스가 아닌 생성자 호출을 통해 새로 생성한 인스턴스 반환하여 변경가능하다.

2. 제네릭으로도 생성가능하다.

3. 정적 팩터리 메서드 참조를 공급자로 사용할 수 있다.

-> 익명함수 등을 이용해 사용 가능

 

단점

1. 클라이언트가 테스팅하기 어렵다.

-> 인터페이스를 통해 해당 클래스를 구현하고 Mock객체를 사용한다.

2. 리플렉션으로 private 생성자를 호출 가능하다.

-> 인스턴스 처음 생성시 인스턴스가 생성됨을 처리하는 필드를 하나 생성하고 만약 필드가 참인데 생성자 호출시 오류 발생시킨다.

3. 역직렬화시 새로운 인스턴스 생성

-> readResolve 메서드 구현으로 해결 

 

 

2. enum을 사용한다.

- 위의 장점을 모두 갖진 않지만 모든 단점을 보완한 방법

 

 

(+) with Spring

Spring에서는 기본적으로 싱글턴 패턴을 제공한다. Controller, Service, DAO 사용시 하나의 인스턴스만을 생성하고 이를 사용한다. 이는 애너테이션을 통해 명시가 가능하며, 일부에서는 Config를 통해 명시가 가능하다. 이 후, Spring에서 자체적으로 인스턴스를 생성 관리한다. 따라서 싱글턴임을 명시해야한다는 부담이 줄어든다. 하지만, 해당 클래스들 이외에, 다른 클래스나 위에서 나온 예시와 같은 경우에서는 다음과 같이 처리하면 좋을 것 같다.

 

 

요약

1. private static final 인스턴스 생성

2. private 생성자 설정

3. 정적 팩터리 메서드 구현으로 인스턴스 반환

4. 인터페이스 구현해 테스팅시 Mock객체 사용

5. private 생성자 안에 오류 처리해 리플렉션 방어

6. readResolve 메서드 구현해 역직렬화시 새로운 인스턴스 생성 억제

 

 

Item 4 : 인스턴스화를 막으려거든 Private 생성자를 활용하라

 

우선적으로, 인스턴스화를 막는경우는 아래와 같다.

- 정적 필드와 정적 메서드만을 모아둔 경우

-> 기본값이나 배열 관련 메서드를 모아둔 경우

-> 특정 인터페이스를 구현한 객체를 생성해주는 정적 메서드를 모아둔 경우

-> final 클래스와 관련된 메서드를 모아놓고 사용하는 경우

 

 

적용 사례

- 해당 코드는 JWT를 생성해주고 점검을 해주는 클래스이다.

- 해당 클래스에서는 인스턴스 생성이 필요하지 않으며, 정적 필드는 없지만 정적 메서드만을 갖는 경우로 위의 예시에 속한다.

- 추가적으로 인스턴스 생성시 오류를 던져줄 수 있다.

public class JWTutil {
  
  private JWTutil() {
    throw new AssertionError()
  }
  
  
  public static String getUserId(String token, String secretKey) {
    return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
        .getBody().get("userId", String.class);
  }
  
  
  public static String getUserRole(String token, String secretKey) {
    return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
        .getBody().get("userRole", String.class);
  }

  
  
  public static boolean isExpired(String token, String secretKey) {
    return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
        .getBody().getExpiration().before(new Date());
  }

  public static String createJWT(String userId, String role, String secretKey, long expireMs) {
    Claims claims = Jwts.claims();
    claims.put("userId", userId);
    claims.put("userRole", role);
    
    return Jwts.builder()
        .setClaims(claims)
        .setIssuedAt(new Date(System.currentTimeMillis()))
        .setExpiration(new Date(System.currentTimeMillis() + expireMs))
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();
  }
}

 

 

Item 5 : 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

 

예를 들어, 사전을 통해 번역작업을 해주는 클래스가 있다고 가정하자. 이 클래스는 한글 사전, 일본어 사전, 영어 사전 등을 이용해 글을 번역해주는 클래스이다. 하지만 이러한 경우, 초기에 한글사전으로 자원을 넣어놓는다면 해당 사전에 의존성이 커져서 교체가 어려워진다. 따라서 책에서는 이러한 경우에는 객체 생성시 필요한 자원을 주입받아 사용하는 것을 권고하고 있다.

 

 

적용 사례

스프링에서는 기본적으로는 제어의 역전이 이루어져 싱글턴 패턴으로 자원을 주입을 한다. 예를 들어 Controller, Service, DAO 등이 그러한 경우이다.

이 때, 각 클래스는 생성자를 두고 인터페이스를 주입받는 것으로 명시를 해둔다. 그렇게 함으로써, 해당 인터페이스를 구현한 클래스들은 모두 자원으로 입력을 받을 수 있다. 여러가지 클래스가 있을 때 애너테이션을 통해 쉽게 자원을 변경 가능하다.

 

예를 들어, MemberController는 MemberService 인터페이스를 구현한 클래스를 자원으로 받는데, MemberService를 구현한 MemberServiceImpl1, MemberServiceImpl2 클래스가 해당 MemberController의 자원으로 유동적으로 들어갈 수 있는 것이다.