본문 바로가기
프로젝트/도서관리 프로젝트

도서관리 프로젝트[4] Controller 3단 분리, 스프링컨테이너, 스프링빈, 인스턴스화

by CodeMango 2023. 6. 21.

Controller 3단분리

 

 

클린 코딩을 위해 Controller의 함수 1개가 하고 있던 역할을 3단 분리 하겠습니다.

 

1. API의 진입 지점으로써 HPPT Body객체로 변환하고 있다. -> Controller

 

2. 현재 유저가 있는지 없는지 등 확인하고 예외 처리 해준다 -> Service

 

3. SQL을 사용해 실제  DB와의 통신 담당한다 -> Repository

 

 

UserController

package com.group.libraryapp.controller.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.service.user.UserService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class UserController {
    //service호출문장
    private final UserService userService;

    private final JdbcTemplate jdbcTemplate;

    public UserController(JdbcTemplate jdbcTemplate) {   //jdbc템플릿 받아서 설정해주는 생성자

        this.jdbcTemplate = jdbcTemplate;
        this.userService = new UserService(jdbcTemplate);
    }

    //C: 유저 저장 API
    @PostMapping("/user") //POST/ user
    public void saveUser(@RequestBody UserCreateRequest request) {  //아무것도 반환할게 없으므로
        userService.saveUser(request);
    }

    //R: 유저조회 API
    @GetMapping("/user")
    //접근제한자     반환타입     메서드이름
    public List<UserResponse> getUsers() { // 반환값 : List<UserResponse> -> 사용자 응답을 담고 있는 UserResponse 객체를 리스트로 반환
        return userService.getUsers();
    }

    //U: 수정
    //json이 들어온걸 객체로 바꾸기 위해서 dto를 만든다. UserUpdateRequest라는 dto
    @PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request) {
        userService.updateUser(request);
        //데이터가 존재하는지 먼저 확인하기
    }
    //D: 삭제
    @DeleteMapping("/user")
    //쿼리가 하나라서 RequestParam사용함
    public void deleteUser(@RequestParam String name) {
        userService.deleteUser(name);
    }

}

 

UserService

package com.group.libraryapp.service.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 saveUser(UserCreateRequest request){
        userRepository.saveUser(request.getName(), request.getAge());
    }

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


    public void updateUser(UserUpdateRequest request) {

        //userRepository에 jdbctemplate과 id 넘겨주기!
        boolean isUserNotExist = userRepository.isUserNotExist(request.getId());
        //user가 존재하지 않는다면 예외처리
        if(isUserNotExist){
            throw new IllegalArgumentException();
        }
        //userRepository에 jdbctemplate, name, id 넘겨주기!
        userRepository.updateUserName(request.getName(), request.getId());
    }


    public void deleteUser(String name){

        //user가 존재하지 않는다면 예외처리
        if(userRepository.isUserNotExist(name)){
            throw new IllegalArgumentException();
        }

        userRepository.deleteUser(name);
}
}

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;
    }

    //1.생성
    public void saveUser(String name, Integer age){
        String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; //mysql에 저장
        jdbcTemplate.update(sql, name, age); //데이터 변경 저장
    }

    //2. 조회
    public List<UserResponse> getUsers() {
        String sql = "SELECT * FROM user";  //유저테이블의 모든 정보를 가져오는 sql
        //ctrl+o : override
        //유저 정보를 UserResponse타입으로 바꿔주는 함수
        //RowMapper + alt+ Enter  = lamda 로 변형
        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. 수정

    // repository에서는 리퀘스트 모든 객체 쓰지 않아도 되므로 그냥 id만 받아옴.
    public boolean isUserNotExist(Long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        //조회됐을 때 결과가 있으면 무조건 0을 반환하고, ?에 값을 넣어주기 (id)
        //존재하지 않는 경우는 조회 결과가 비어있는 경우(존재하면 0이 나옴)
        //query : 반환된 값들을 List로 감싸줌
        //0은 최종적으로 List로 반환됨 (  [0]  )
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();

    }

    public void updateUserName(String name, long id) {
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        //특정 id를 가진 유저가 특정 이름으로 변경되어야함.
        jdbcTemplate.update(sql, name, id);
    }

    //4. 삭제
    public boolean isUserNotExist(String name){
        String readSql = "SELECT * FROM user WHERE name = ?"; //삭제는 이름을 기준으로 검색해야함
        //조회됐을 때 결과가 있으면 무조건 0을 반환하고, ?에 값을 넣어주기 (id)
        //존재하지 않는 경우는 조회 결과가 비어있는 경우(존재하면 0이 나옴)
        //query : 반환된 값들을 List로 감싸줌
        //0은 최종적으로 List로 반환됨 (  [0]  )
        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);

    }
}

 

이렇게 3단계로 나눴는데, UserController에 의아한 점이 있습니다.

UserController는 JdbcTemplate에 의존하고 있는데요.

지금까지 JdbcTemplate이란 클래스를 설정해준 적도 인스턴스화 해준적도 없습니다.

어떻게 JdbcTemplate을 가져올 수 있었는지 의문이 듭니다.

 

그 비밀은 바로 RestController에 있습니다!

RestController는 UserController 클래스를 API의 진입지점으로 만들 뿐 아니라

UserController 클래스를 *스프링빈으로 등록시킵니다.

 

 


*스프링 컨테이너

스프링은 서버가 시작될 때 내부에 거대한 컨테이너를 생성하며, 이 컨테이너는 애플리케이션의 구성 요소를 관리하고 빈으로 등록된 클래스들을 *인스턴스화합니다.

스프링 컨테이너는 애플리케이션의 실행을 담당하는 주체입니다. 서버가 시작되면 스프링 컨테이너는 설정 파일 또는 어노테이션 기반의 설정을 기반으로 애플리케이션의 빈을 생성하고 관리합니다. 

이때 스프링 컨테이너는 클래스의 인스턴스화를 담당합니다.

스프링 컨테이너는 자바의 리플렉션(Reflection)을 사용하여 클래스를 동적으로 로드하고, 필요한 빈을 생성합니다. 

이때 클래스의 생성자를 호출하여 객체를 인스턴스화하는데, 이 과정에서 스프링 컨테이너는 의존성 주입(Dependency Injection)을 통해 필요한 의존 객체들을 주입합니다.


*의존성 주입

의존성 주입은 스프링이 객체 간의 의존 관계를 자동으로 해결해주는 기능입니다.

UserController에 JdbcTemplate을 주입하기 위해서는 JdbcTemplate을 스프링 빈으로 등록해야 합니다.

이를 위해 RestController 클래스에 UserController를 스프링 빈으로 등록하는 어노테이션 (예: @RestController, @Component, @Controller) 중 하나가 사용됩니다.


따라서, 위 코드에서 UserController 클래스의 생성자를 직접 호출하지 않더라도, 스프링 컨테이너가 서버가 시작될 때 해당 클래스의 인스턴스를 생성하고, 의존성 주입을 통해 필요한 의존 객체를 주입합니다.

이렇게 생성된 객체는 스프링 컨테이너 내부에 저장되고, *스프링 빈으로 관리됩니다.

 

*인스턴스화

자바에서 인스턴스화(Instantiation)란 클래스로부터 객체(인스턴스)를 생성하는 과정을 말합니다. 
클래스는 객체를 생성하기 위한 설계도 역할을 하며, 실제로 동작하는 코드는 객체를 통해 사용됩니다.

인스턴스화를 통해 생성된 객체는 특정 클래스의 멤버 변수와 메서드에 접근할 수 있습니다. 

객체를 생성하기 위해서는 new 키워드를 사용하고, 생성자(Constructor)를 호출하여 객체를 초기화합니다.

일반적으로 클래스의 멤버 변수는 객체마다 개별적인 값을 가지고 있으며, 메서드는 객체의 특정 동작을 수행합니다. 

따라서 static 키워드가 사용되지 않은 일반적인 멤버 변수나 메서드에 접근하려면 객체를 생성하여 해당 객체의 멤버로 접근해야 합니다.

예를 들어, 다음은 MyClass라는 클래스로부터 객체를 생성하고, 인스턴스 변수와 인스턴스 메서드에 접근하는 예입니다

public class MyClass {
    private int myVariable;  // 인스턴스 변수

    public void myMethod() {  // 인스턴스 메서드
        // 메서드 내용
    }

    public static void main(String[] args) {
        // MyClass 객체 인스턴스화
        MyClass myObject = new MyClass();

        // 인스턴스 변수에 접근
        myObject.myVariable = 10;

        // 인스턴스 메서드 호출
        myObject.myMethod();
    }
}

위 코드에서 myObject라는 객체를 생성하여 해당 객체의 myVariable 인스턴스 변수에 접근하고, myMethod 인스턴스 메서드를 호출하였습니다.

따라서 static이 아닌 코드를 사용하려면 인스턴스화를 통해 객체를 생성하고, 생성된 객체를 통해 해당 코드에 접근해야 합니다.


*스프링 빈

스프링 빈(Spring Bean)은 스프링 프레임워크에서 관리되는 객체를 말합니다. 스프링은 객체의 생명주기를 관리하고, 필요에 따라 객체를 생성하고 소멸시키는 등의 작업을 수행합니다.

 

스프링은 객체를 빈(Bean)으로 등록하기 위해 특정 어노테이션을 사용합니다. 

일반적으로 @Component, @Service, @Repository, @Controller 등의 어노테이션을 이용하여 클래스를 스프링 빈으로 등록할 수 있습니다. 

이렇게 등록된 빈은 스프링 컨테이너에 의해 관리되며, 필요한 곳에서 해당 빈을 주입받아 사용할 수 있습니다.

 

위 코드에서 UserController가 JdbcTemplate에 의존하고 있는데,

이는 JdbcTemplate이 UserController의 필수적인 기능을 수행하기 위해 사용된다는 것을 의미합니다.

스프링에서는 이러한 의존성을 관리하기 위해 *의존성 주입(Dependency Injection)을 사용합니다.

 

따라서 RestController에 의해 UserController가 스프링 빈으로 등록되고, JdbcTemplate은 다른 구성 요소(예: XML 구성 파일, Java Config 등)에서 설정되어 해당 UserController 스프링 빈에 주입되게 됩니다.

이로써 UserController는 JdbcTemplate을 사용할 수 있게 되는 것입니다.

 

스프링은 이렇게 의존성 주입을 통해 객체 간의 결합도를 낮추고 유연성과 테스트 용이성을 향상시킬 수 있습니다. 

또한, 스프링 빈으로 등록된 객체들은 스프링 컨테이너에 의해 관리되므로, 필요한 시점에 자동으로 생성되고 소멸됩니다.

 


<요점 정리>

1. 스프링 컨테이너

스프링부트로 만들어진 서버가 실행되면 스프링 서버 내부에 거대한 컨테이너가 만들어집니다.

그리고 이 상자 안에는 여러 클래스가 들어갑니다. 

2. 스프링 빈

기본적으로 많은 스프링 빈들이 등록됩니다.

이때 빈으로 등록된 클래스들이 인스턴스화됩니다.

3. 내가 설정한 스프링 빈 등록

4. 필요한 의존성 자동 설정

 


스프링 빈으로 등록하기

그렇다면 왜 UserRepository는 JdbcTemplate을 가져오지 못할까요?

JdbcTemplate을 바로 가져오려면 UserRepository가 스프링 빈이어야 하니고 그냥 클래스이기 때문입니다.

바꿔말하면, UserRepository를 스프링 빈으로 등록하면 JdbcTemplate을 바로 가져올 수 있게 됩니다.

 

빈을 등록하는 방법

@Service, @Repository

는 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용합니다.

 

반면, @Configuration + @Bean은 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용합니다.

@Repository

UserRepository를 스프링빈으로 등록하려면 @Repository 어노테이션을 붙여줍니다.

그러면 초록색 콩모양이 생성됨과 동시에 UserRepository 가 스프링 빈으로 등록됩니다!

@Service

UserService도 마찬가지로 어노테이션을 붙여 스프링 빈으로 등록합니다.

 

🎈순서를 정리하자면

1. 스프링 컨테이너 시작되고 기본적인 스프링 빈들이 등록됩니다.

2. JdbcTemplate을 의존하는 userRepository가 스프링 빈으로 등록되면서 인스턴스화됩니다.

3. UserRepository를 의존하는 UserService가 스프링 빈으로 등록됩니다. 

4. UserService를 의존하는 UserController가 스프링 빈으로 등록됩니다.

 


 

이렇게 해서 다시 쓰여진 코드는 아래와 같습니다

 

UserController

package com.group.libraryapp.controller.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.service.user.UserService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class UserController {
    //service호출문장
    private final UserService userService;

    public UserController(UserService userService) {   //jdbc템플릿 받아서 설정해주는 생성자
        this.userService = userService;
    }

    //C: 유저 저장 API
    @PostMapping("/user") //POST/ user
    public void saveUser(@RequestBody UserCreateRequest request) {  //아무것도 반환할게 없으므로
        userService.saveUser(request);
    }

    //R: 유저조회 API
    @GetMapping("/user")
    //접근제한자     반환타입     메서드이름
    public List<UserResponse> getUsers() { // 반환값 : List<UserResponse> -> 사용자 응답을 담고 있는 UserResponse 객체를 리스트로 반환
        return userService.getUsers();
    }

    //U: 수정
    //json이 들어온걸 객체로 바꾸기 위해서 dto를 만든다. UserUpdateRequest라는 dto
    @PutMapping("/user")
    public void updateUser(@RequestBody UserUpdateRequest request) {
        userService.updateUser(request);
        //데이터가 존재하는지 먼저 확인하기
    }
    //D: 삭제
    @DeleteMapping("/user")
    //쿼리가 하나라서 RequestParam사용함
    public void deleteUser(@RequestParam String name) {
        userService.deleteUser(name);
    }

}

 

UserService

package com.group.libraryapp.service.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 org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;

    }

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


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


    public void updateUser(UserUpdateRequest request) {

        //userRepository에 jdbctemplate과 id 넘겨주기!
        boolean isUserNotExist = userRepository.isUserNotExist(request.getId());
        //user가 존재하지 않는다면 예외처리
        if(isUserNotExist){
            throw new IllegalArgumentException();
        }
        //userRepository에 jdbctemplate, name, id 넘겨주기!
        userRepository.updateUserName(request.getName(), request.getId());
    }


    public void deleteUser(String name){

        //user가 존재하지 않는다면 예외처리
        if(userRepository.isUserNotExist(name)){
            throw new IllegalArgumentException();
        }

        userRepository.deleteUser(name);
}
}

 

UserRepository

package com.group.libraryapp.repository.user;

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

import java.util.List;

@Repository
public class UserRepository {

    private final JdbcTemplate jdbcTemplate;

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

    //1.생성
    public void saveUser(String name, Integer age){
        String sql = "INSERT INTO user (name, age) VALUES (?, ?)"; //mysql에 저장
        jdbcTemplate.update(sql, name, age); //데이터 변경 저장
    }

    //2. 조회
    public List<UserResponse> getUsers() {
        String sql = "SELECT * FROM user";  //유저테이블의 모든 정보를 가져오는 sql
        //ctrl+o : override
        //유저 정보를 UserResponse타입으로 바꿔주는 함수
        //RowMapper + alt+ Enter  = lamda 로 변형
        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. 수정

    // repository에서는 리퀘스트 모든 객체 쓰지 않아도 되므로 그냥 id만 받아옴.
    public boolean isUserNotExist(Long id){
        String readSql = "SELECT * FROM user WHERE id = ?";
        //조회됐을 때 결과가 있으면 무조건 0을 반환하고, ?에 값을 넣어주기 (id)
        //존재하지 않는 경우는 조회 결과가 비어있는 경우(존재하면 0이 나옴)
        //query : 반환된 값들을 List로 감싸줌
        //0은 최종적으로 List로 반환됨 (  [0]  )
        return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, id).isEmpty();

    }

    public void updateUserName(String name, long id) {
        String sql = "UPDATE user SET name = ? WHERE id = ?";
        //특정 id를 가진 유저가 특정 이름으로 변경되어야함.
        jdbcTemplate.update(sql, name, id);
    }

    //4. 삭제
    public boolean isUserNotExist(String name){
        String readSql = "SELECT * FROM user WHERE name = ?"; //삭제는 이름을 기준으로 검색해야함
        //조회됐을 때 결과가 있으면 무조건 0을 반환하고, ?에 값을 넣어주기 (id)
        //존재하지 않는 경우는 조회 결과가 비어있는 경우(존재하면 0이 나옴)
        //query : 반환된 값들을 List로 감싸줌
        //0은 최종적으로 List로 반환됨 (  [0]  )
        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);

    }
}

댓글