들어가며
최근에 코드를 보면서 느낀 점들이 있어서, 간단하게 정리해보려고 한다. 지금부터 할 이야기들은 반드시 맞는 것은 아니고, 이런 식으로 작업한 것들이 좋지 않을 수 있다는 의도로 받아들여주면 좋을 거 같다.
자바 버전은 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 객체를 조회한다.위와 같은 코드를 작성할 일이 빈번하게 되는데, 작업을 진행하다 보면 여러 번 람다 식을 활용하게 되고 이는 가독성에 안 좋은 영향을 미치게 되는 경우도 있다. 특히
- 객체를 DB에서 조회
- 객체에서 키 추출
- 2에서 추출한 키를 가지고 객체 조회
- 3에서 조회한 객체에서 키 추출
과 같은 사이클이 길어지면 코드를 읽기 어렵게 되고, 중간에 필터와 같은 다른 조건이라도 들어가게 되면 가독성이 더 하락하게 된다.
내가 사용하는 방법은 해당 람다식을 메서드로 묶는 것이다.
List<Article> articleList = articleService.selectArticleList(articleQueryObject);
List<Integer> userNoList = extractUserNoList(articleList); // 이 안에 람다식을 집어 넣는다.
List<User> userList = userService.selectUserListByUserNoList(userNoList); // userNoList를 가지고 User 객체를 조회한다.다음과 같이 람다식을 메서드로 추출하면 가독성을 향상시킬 수 있다.
리퀘스트 받는 객체와. DB insert 객체, 응답 객체는 각기 다른 객체를 사용한다.
웹 사이드 서버 프로그래밍에서는 다음과 같은 비즈니스 로직을 구현할 일이 많다.
- 클라이언트로부터 요청을 받는다.
- DB에서 해당 객체를 조회한다.
- 응답 객체를 생성하여 반환한다.
위의 프로세스는 다음과 같이 처리할 수도 있다.
@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 객체는 다음과 같은 일을 수행한다.
- 요청을 받는다.
- DB에 insert
- 화면을 그리는 데이터를 반환한다.
하나의 객체가 하는 일이 너무 많기 때문에 다음과 같은 문제가 발생한다.
객체를 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 객체를 반환한다.
}
}한 객체가 한 가지 일만 수행하게 만든다면, 위의 문제를 해결할 수 있다.