들어가며

최근에 코드를 보면서 느낀 점들이 있어서, 간단하게 정리해보려고 한다. 지금부터 할 이야기들은 반드시 맞는 것은 아니고, 이런 식으로 작업한 것들이 좋지 않을 수 있다는 의도로 받아들여주면 좋을 거 같다.

자바 버전은 JDK 8.0 이상이고, 스프링 프레임워크를 사용하고 있다.

파라미터의 숫자가 많으면 객체로 변환한다.

다음과 같은 코드가 있다고 가정하자

public List<Article> selectArticleList(int size, int pageNumber, int userNo) {
    // Article을 List 형태로 조회한다.
}

게시글을 페이징하는 코드가 있다고 가정하자. 웹 엔터프라이즈 환경에서 페이징을 하는 일은 비교적 빈번하며, 위와 같은 코드를 작성할 일도 많다. 위와 같은 코드는 int 형태의 파라미터를 여러 개 받게 되는데, 같은 형이다보니 순서를 잘못 입력하기 쉽다.

selectArticleList(20, 1, 1001); // userNo가 1001인 유저의 게시글 중 1페이지를 조회한다. 한 페이지에 게시글 노출 개수는 20개이다.
 
selectArticleList(1, 20, 1001); // 앗, 실수했다!

위와 같은 실수를 방지하기 위해서, 파라미터를 객체로 변환하여 받으면 좋다.

class ArticleQueryObject {
    private int size;
    private int pageNumber;
    private int userNo;
 
    // getter, setter ...
}
 
public List<Article> selectArticleList(ArticleQueryObject articleQueryObject) {
    // 구현
}

위와 같이 객체로 변환하여 넘기게 되면 실수를 줄일 수 있다.

그러면, 언제 파라미터를 객체로 변환하는가? 내 기준은 다음과 같다.

  • 파라미터의 개수가 3개 이상이 된다.
  • 비슷한 형태의 파라미터로 데이터 조회 레이어에서 지속적으로 리퀘스트를 한다.

위의 두 가지 조건 중 하나를 충족한다면, 요청 데이터를 객체로 변환하여 넘겨주는 편이다.

데이터를 변환하는 람다식은 메서드로 묶는다.

Java 8 에서 람다식이 추가되었고, Java 9 가 나온 현재 람다식은 많은 부분에 사용되고 있다. 유용하긴 하지만 가독성을 해치는 부분이 있다고 생각하고, 그런 경우는 주의하면 좋을 거 같다.

다음과 같은 경우가 있을 수 있다. 최근 대용량을 다루는 웹 프로젝트에서는 가급적이면 테이블간 join 을 배제하고, 한 테이블을 조회한 후에 다른 테이블의 key 를 가지고 조회하는 형태를 취하고 있다. 그렇게 되면 다음과 같은 코드가 생긴다.

List<Article> articleList = articleService.selectArticleList(articleQueryObject);
 
List<Integer> userNoList = articleList.stream().map(article -> {
                                            return article.getUserNo();
                                        }).collect(Collectors.toList()); // 게시물에서 userNo를 추출한다.
 
List<User> userList = userService.selectUserListByUserNoList(userNoList); // userNoList를 가지고 User 객체를 조회한다.

위와 같은 코드를 작성할 일이 빈번하게 되는데, 작업을 진행하다 보면 여러 번 람다 식을 활용하게 되고 이는 가독성에 안 좋은 영향을 미치게 되는 경우도 있다. 특히

  1. 객체를 DB에서 조회
  2. 객체에서 키 추출
  3. 2에서 추출한 키를 가지고 객체 조회
  4. 3에서 조회한 객체에서 키 추출

과 같은 사이클이 길어지면 코드를 읽기 어렵게 되고, 중간에 필터와 같은 다른 조건이라도 들어가게 되면 가독성이 더 하락하게 된다.

내가 사용하는 방법은 해당 람다식을 메서드로 묶는 것이다.

List<Article> articleList = articleService.selectArticleList(articleQueryObject);
 
List<Integer> userNoList = extractUserNoList(articleList); // 이 안에 람다식을 집어 넣는다.
 
List<User> userList = userService.selectUserListByUserNoList(userNoList); // userNoList를 가지고 User 객체를 조회한다.

다음과 같이 람다식을 메서드로 추출하면 가독성을 향상시킬 수 있다.

리퀘스트 받는 객체와. DB insert 객체, 응답 객체는 각기 다른 객체를 사용한다.

웹 사이드 서버 프로그래밍에서는 다음과 같은 비즈니스 로직을 구현할 일이 많다.

  1. 클라이언트로부터 요청을 받는다.
  2. DB에서 해당 객체를 조회한다.
  3. 응답 객체를 생성하여 반환한다.

위의 프로세스는 다음과 같이 처리할 수도 있다.

@RestController
class ArticleController {
 
   @RequestMappling("/articles")
   public Article insertArticle(@ModelAttribute Article article) {
       bindArticleProperty(article); // article 객체에 필요한 프로퍼티를 DB에서 조회하여 binding 한다.
       artcieService.insertArticle(article); // article 객체를 insert한다.
       return article; // 데이터를 그리기 위해 화면에 반환한다.
   }
}

얼핏 보면 문제없이, 하나의 객체로 조화롭게 비즈니스 로직을 처리한 거 같다. 그러나, 위의 코드에는 문제점이 있다.

하나의 객체는 한 가지 일을 해야 한다

한 가지 객체는 한 가지 일을 해야 한다. 이는 객체지향 5대 원칙에서 SRP 라고 하는데, 한 객체는 한 가지 일만 해야 한다는 것이다.

위의 코드에서 Article 객체는 다음과 같은 일을 수행한다.

  1. 요청을 받는다.
  2. DB에 insert
  3. 화면을 그리는 데이터를 반환한다.

하나의 객체가 하는 일이 너무 많기 때문에 다음과 같은 문제가 발생한다.

객체를 DB에 insert 할때 insert 할 값들만 따로 매핑해주어야 한다.

article 객체의 모든 데이터를 DB에 보관하지는 않는다. 그렇기 때문에 위와 같은 코드에서는 DB에 insert 할때 다음과 같은 코드가 필요하다.

public int insertArticle(Article article) {
    Map<String, Ojbect> param = new HashMap<>();
    param.put("articleId", article.getArticle());
    param.put("contents", article.getContents());
    // ... 쿼리 파라미터를 세팅하는 작업 진행
}

어떤 라이브러리를 사용하냐에 따라 다르겠지만, 공통적으로 위와 같은 코드는 좋지 않다. 객체로 변환해서 insert 하면 된다고 이야기 할 수 있다. 그런 경우라면 @ModelAttribute 로 리퀘스트 객체를 받은 다음에, 그 객체를 Article 객체로 변환하는 것이 더 좋다.

데이터를 클라이언트로 반환시에 추가 코드가 필요하다

Article 객체의 모든 데이터를 클라이언트로 내려줄 수 없다. Article 객체에는 다음과 같은 코드도 존재할 수 있다.

public boolean isValidArticle() {
    // Article이 유효한지 체크하는 코드 존재
}

위 값을 클라이언트로 내려주고 싶지 않을 수 있다. 응답 규격이 JSON이고 Jackson 라이브러리를 사용한다고 하면, 다음과 같은 어노테이션을 추가할 수 있다.

@JsonIgnore
public boolean isValidArticle() {
    // Article이 유효한지 체크하는 코드 존재
}

객체의 프로퍼티가 몇 개 없는 경우에는 별 문제가 없지만, 객체 내부의 프로퍼티가 많은 경우에는 어떤 프로퍼티가 클라이언트로 반환되는지 알기 어려울 수 있다.

위의 문제들을 해결하는 방법은 간단하다. 레이어마다 각기 다른 객체를 사용하라.

@RestController
class ArticleController {
 
   @RequestMappling("/articles")
   public ArticleResponse insertArticle(@ModelAttribute Article articleRequest) {
       Article article = convertArticle(articleRequset); // 요청 객체를 Article 객체로 변환한다.
       artcieService.insertArticle(article); // article 객체를 insert한다.
       return new ArticleResponse(article); // 화면에 그리기 위해 ArticleResponse 객체를 반환한다.
   }
}

한 객체가 한 가지 일만 수행하게 만든다면, 위의 문제를 해결할 수 있다.