본문 바로가기
기록해보기

Controller를 3단 분리하기 -Service와 Repository

by titlejjk 2023. 6. 5.

왜 코드를 깔끔하게 작성해야하는지에 대해 생각해 봤는데 여지껏 써왔던 Controller의 함수의 역할을 짚어보자

package com.group.libraryapp.controller.user;

import com.group.libraryapp.dto.user.request.UserCreateRequest;
import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.dto.user.response.UserResponse;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.*;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {

    private final JdbcTemplate jdbcTemplate;

    public UserController(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate = jdbcTemplate;
    }
    @PostMapping("/user") //POST /user
    public void saveUser(@RequestBody UserCreateRequest request){
        String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
        jdbcTemplate.update(sql, request.getName(), request.getAge());
    }

    @GetMapping("/user")
    public List<UserResponse> getUser(){
        String sql = "SELECT * FROM user";
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            long id = rs.getLong("id");
            String name = rs.getString("name");
            int age = rs.getInt("age");
            return new UserResponse(id, name, age);
        });
    }

    @PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request){
        String readSql = "SELECT * FROM user WHERE id = ?";
        boolean isUserNotExist = jdbcTemplate.query(readSql, (rs,rowNum) -> 0, request.getId()).isEmpty();
        if(isUserNotExist){
            throw new IllegalArgumentException();
        }
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, request.getName(), request.getId());
    }

    @DeleteMapping("/user")
    public void deleteUser(@RequestParam String name){
        String readSql = "SELECT * FROM user WHERE name = ?";
        boolean isUserNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty();
        if(isUserNotExist){
            throw new IllegalArgumentException();
        }
        String sql = "DELETE FROM user WHERE name =?";
        jdbcTemplate.update(sql, name);
    }

}
  1. API의 진입 지점으로써 HTTP Body를 객체로 변환하고 있다.
  2. 현재 유저가 있는지 없는지 등을 확인하고 예외 처리를 해준다.
  3. SQL을 사용해 실제 DB와의 통신을 담당한다.

이 Controller는 세가지의 역할을 하고 있는데 위에 세가지 기능을 나누어 줄 것이다. 1번의 기능은 Controller에 남겨둘 것이고 그 외에 나머지 기능인 2번은 Service로 3번은 Repository의 역할로 나눌 것이다.

이렇게 계획을 잡고 수정하는 API먼저 분리해보겠다.

가장 먼저해야할 것은 두가지의 Class를 만드는 것이다.

service, repository라는 Package를 만들어 준후에 

service 안에 user Package를 만들어 그안에 UserService라는 java Class를 만들어주겠다.

여기 UserService에는 현재 유저가 있는지, 없는지 등을 확인하는 예외 처리를 넣어줄 것이다.

먼저 함수를 만들어주고..

현재 Controller중에 API와 Mapping 되는 부분 그다음 HTTP Body를 객체로 변환하는 부분을 남겨두고 전체를 복사해서 Service로 붙여넣기 해주겠다.

위 코드처럼 오류 부분이 나오니 정보들을 넣어주자..

먼저 JdbcTemplate를 매개 변수로 넣어주고 그 다음에 UserUpdateRequset도 넣어주자

package com.group.libraryapp.service.user;

import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import org.springframework.jdbc.core.JdbcTemplate;

public class UserService {

    public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request){
        String readSql = "SELECT * FROM user WHERE id = ?";
        boolean isUserNotExist = jdbcTemplate.query(readSql, (rs,rowNum) -> 0, request.getId()).isEmpty();
        if(isUserNotExist){
            throw new IllegalArgumentException();
        }
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, request.getName(), request.getId());
    }

}

 

하나의 특징으로는 Controller의 같은 경우 Controller의 역할을 수행 하기 위해서 @RequestBody가 있지만 UserService는 Controller가 객체로 반환 한 것을 그냥 받을 것이기 때문에 그냥 UserUpdate Request 객체만 받는다. 앞에 어노테이션이 붙지 않는 것 이다.

이렇게 작성하고 다시 Controller로가서 UserSerive를 호출하게 해주자.

이렇게 userService를 만들어 준 후에 밑에 updateUser로 가서 호출하기만 하면된다.

@PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request){
        userService.updateUser(jdbcTemplate, request);
    }

이렇게 2단분리까지는 성공했다. 이제 service를 service와 repository로 분리를 해 보겠다.

UserService처럼 동일한 방법으로 UserRepository Class를 만들어 준다.

이 Repository의 기능은 SQL을 사용해 실제 DB와의 통신을 담당하게 할 것이다.

첫 번째

String readSql = "SELECT * FROM user WHERE id = ?";
        boolean isUserNotExist = jdbcTemplate.query(readSql, (rs,rowNum) -> 0, request.getId()).isEmpty();

이 두줄을 UserRepository로 복붙해준다. 그전에 UserRepository에 boolean type의 메소드를 만들어 준다.

package com.group.libraryapp.repository.user;

public class UserRepository {

    public boolean isUserNotExist(){
        String readSql = "SELECT * FROM user WHERE id = ?";
        boolean isUserNotExist = jdbcTemplate.query(readSql, (rs,rowNum) -> 0, request.getId()).isEmpty();
    }
}

빨간줄 제거!

그 다음 에러를 없애기 위해 

JdbcTemplate와 request를 써주는데 여기서 한가지 고민할 부분이 있다. request 객체를 Repository에 그대로 줄지 아니면 Id만 필요하니 user id만 받을지 선택해 봐야하는데 강사님의 경우 Repository 까지 내려오게 되면 Request에 있는 모든 필드가 쓰이는게 아니다 보니 특정필드만 써주는 것을 선호하신다니 이렇게 진행하겠다.

isUserNotExist메소드는 결국 하는 역할은 주어진 user id를 받아 jdbcTemplate를 통해 그 user가 있는지 확인하고 있으면 false 없으면 true를 반환하는 하는 역할을 하기에 변수 없이 바로 반환하게 끔 해주면된다.

package com.group.libraryapp.repository.user;

import org.springframework.jdbc.core.JdbcTemplate;

public class UserRepository {

    public boolean isUserNotExist(JdbcTemplate jdbcTemplate, long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs,rowNum) -> 0, id).isEmpty();
    }
}

이제 UserService Class로 가서 repository를 사용해야하기 때문에 Controller와 마찬가지로 안에

private final UserRepository userRepository = new UserRepository();

 를 선언해주고 기존에 있던 메소드의 구현부에

boolean isUserNotExist = userRepository.isUserNotExist(jdbcTemplate, request.getId());

위에 코드로 대체해준다.

보면 userRepository가 DB와의 통신을 담당하고 user가 있는지 없는지 확인해주는 역할을 가지고 있기에 그냥 가져다 쓰면된다. 뒤에는 jdbcTemplate와 request.getId의 정보가 필요하기 때문에 작성해주었다. 여기서 service코드를 더 짧고 컴팩트하게 만들어주게 하려면

public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request){
        
        if(userRepository.isUserNotExist(jdbcTemplate, request.getId())){
            throw new IllegalArgumentException();
        }
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, request.getName(), request.getId());
    }

이렇게 if문 안으로 넣어주면 더 좋을 듯 하다.

 

다음으로 Update SQL도 repository로 넘겨줘 보겠다.

먼저 메소드 이름을 updateUserName이라 만들어주고 JdbcTemplate와 name, id를 받게끔 만들어 준 후에

package com.group.libraryapp.repository.user;

import org.springframework.jdbc.core.JdbcTemplate;

public class UserRepository {

    public boolean isUserNotExist(JdbcTemplate jdbcTemplate, long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs,rowNum) -> 0, id).isEmpty();
    }

    public void updateUserName(JdbcTemplate jdbcTemplate, String name, long id){

    }
}

구현부에 똑깥이 SQL문을 써준 후 완성해준다.

public void updateUserName(JdbcTemplate jdbcTemplate, String name, long id){
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, name, id);
    }

userRepository에 updateUserName은 이름만 봐도 알수 있듯이 user저장소 즉 DB와 접근해서 SQL의 정보를 넘겨주는 기능을 하는 것인데 이 기능이 user의 이름 어떤번호를 넣어 수정할 것인지 나타내는 함수를 만들었다.

이렇게 하고 이 메소드를 다시 Service에서 호출하게 끔 해주면..

package com.group.libraryapp.service.user;

import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.repository.user.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;

public class UserService {

    private final UserRepository userRepository = new UserRepository();

    public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request){

        if(userRepository.isUserNotExist(jdbcTemplate, request.getId())){
            throw new IllegalArgumentException();
        }

        userRepository.updateUserName(jdbcTemplate, request.getName(), request.getId());
    }

}

이 되겠다.

 

여기까지 Repository의 역할을 정리해보자면 원래 한 함수에서 세가지 역할을 하고 있던 다소 깔끔하지 않은 코드를

강사님 ppt에서 퍼옴

이렇게 정리해 줄수 있다.

API와 HTTP 역할을 담당하는 Controller

분기처리나 로직을 담당하는 Service

그리고 DB와의 접근을 담당하는 Repository

DTO는 이러한 계층간의 정보를 제공한다.

 

이런구조를 Layered Architecture라고 한다.

여기까지 3단 분리를 했지만 여기서 살짝 불편한 점이 있는데 여지껏 코드를 작성하면서 JdbcTemplate을 계속 파라미터로 넘겨주었는데 이것을 해결하기 위해서 jdbcTemplate을 변수로 넘겨주면 해결이 된다.

UserRepository Class에서 JdbcTemplate을 선언해주고 생성자를 만들어준다.

package com.group.libraryapp.repository.user;

import org.springframework.jdbc.core.JdbcTemplate;

public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public boolean isUserNotExist(JdbcTemplate jdbcTemplate, long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs,rowNum) -> 0, id).isEmpty();
    }

    public void updateUserName(JdbcTemplate jdbcTemplate, String name, long id){
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, name, id);
    }
}

그 다음 UserService에도 생성자를 하나 만든 후에 JdbcTemplate를 받는다.

package com.group.libraryapp.service.user;

import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.repository.user.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;

public class UserService {

    private final UserRepository userRepository;
    public UserService(JdbcTemplate jdbcTemplate){
        userRepository = new UserRepository(jdbcTemplate);
    }

    public void updateUser(JdbcTemplate jdbcTemplate, UserUpdateRequest request){

        if(userRepository.isUserNotExist(jdbcTemplate, request.getId())){
            throw new IllegalArgumentException();
        }

        userRepository.updateUserName(jdbcTemplate, request.getName(), request.getId());
    }

}

그 다음 똑같이 UserController에서 UserService를 만들어줄 때

this.userService = new UserService(jdbcTemplate);

를 넣어준다.

이해는 가는데 복잡하다..

원래는 jdbcTemplate을 계속해서 Controller에서 Service로 Service에서 Repository로  함수를 통해 전달해주었는데

UserRepository에 변수로 넣어둔 다음에 생성자를 통해 UserRepository를 인스턴스화 시키는 시점에 jdbcTemplate을 넣어주는 형식으로 바꾸었다.

 

이제 삭제, 생성, 조회 API까지 분리하러 가보겠다.

UserController

package com.group.libraryapp.controller.user;

import com.group.libraryapp.dto.user.request.UserCreateRequest;
import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.dto.user.response.UserResponse;
import com.group.libraryapp.service.user.UserService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.web.bind.annotation.*;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {

    private final UserService userService;
    private final JdbcTemplate jdbcTemplate;

    public UserController(JdbcTemplate jdbcTemplate){
        this.jdbcTemplate = jdbcTemplate;
        this.userService = new UserService(jdbcTemplate);
    }
    @PostMapping("/user") //POST /user
    public void saveUser(@RequestBody UserCreateRequest request){
        userService.saveUser(request);
    }

    @GetMapping("/user")
    public List<UserResponse> getUser(){
        return userService.getUsers();
    }

    @PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request){
        userService.updateUser(request);
    }

    @DeleteMapping("/user")
    public void deleteUser(@RequestParam String name){
        userService.deleteUser(name);
    }

}

UserService

package com.group.libraryapp.service.user;

import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.dto.user.request.UserCreateRequest;
import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.dto.user.response.UserResponse;
import com.group.libraryapp.repository.user.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

public class UserService {

    private final UserRepository userRepository;

    public UserService(JdbcTemplate jdbcTemplate){
        userRepository = new UserRepository(jdbcTemplate);
    }

    public void updateUser(UserUpdateRequest request){

        if(userRepository.isUserNotExist(request.getId())){
            throw new IllegalArgumentException();
        }

        userRepository.updateUserName(request.getName(), request.getId());
    }

    public void deleteUser(String name){
        if(userRepository.isUserNotExist(name)){
            throw new IllegalArgumentException();
        }

        userRepository.deleteUser(name);
    }

    public void saveUser(UserCreateRequest request){
        userRepository.saveUser(request.getName(),request.getAge());
    }

    public List<UserResponse> getUsers(){
        return userRepository.getUsers();
    }
}

UserRepository

package com.group.libraryapp.repository.user;

import com.group.libraryapp.dto.user.response.UserResponse;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public boolean isUserNotExist(long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.query(readSql, (rs,rowNum) -> 0, id).isEmpty();
    }

    public void updateUserName(String name, long id){
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, name, id);
    }

    public boolean isUserNotExist(String name){
        String readSql = "SELECT * FROM user WHERE name = ?";
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty();
    }

    public void deleteUser(String name){
        String sql = "DELETE FROM user WHERE name =?";
        jdbcTemplate.update(sql, name);
    }

    public void saveUser(String name, Integer age){
        String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
        jdbcTemplate.update(sql, name, age);
    }

    public List<UserResponse> getUsers(){
        String sql = "SELECT * FROM user";
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            long id = rs.getLong("id");
            String name = rs.getString("name");
            int age = rs.getInt("age");
            return new UserResponse(id, name, age);
        });
    }
}

이렇게 3단분리를 완료했다.

다하고 import도 정리해주었다 맥에서는 ^ ⌥ O단축키를 이용하면된다.
추가로 개인적인 취향이긴 한데 강사님 같은경우는 이 순서를 통일한다.

생성 조회 업데이트 삭제 이 CRUD이 순서로 작성해준다.(Controller/Service/Repository 에서)

이 때 메소드를 옮겨줄수 있다. 단축키는 ⌘ ⇧ ↑,↓를 사용하면된다.

 

이제 기능이 잘 동작하는지 확인해 보겠다.

여기서 궁금한점이 있다.

Controller에서 JdbcTemplate은 어떻게 가져온 걸까?

더 나아가서 현재는 Controller에서 JdbcTemplate을 Service로 주고 그걸 다시 Repository주고있는데 바로 Repository에서 받을 순 없을까?

이 부분은 내일해보겠다..

졸려

댓글