본문 바로가기
Spring

Spring 기초 4 (MVC) (JPA) (BEAN) (@Componet @Autowired) (Hibernate)

by sehunbang 2024. 1. 20.

현 메모장 CONTROLLER 문제

클라스 하나로 모든 API 를 처리 하고 있음, 함수가 많아 질수록 코드가 더러워 보이고 복잡해집니다.

코드드 추가 밑 변경이 자주있는 작업인데 복잡한 코드 보면서 하는데 무리가 있음.

서버 관련 작업은 비슷하다고 해서

 

1컨드롤러 2서비스 3레포지터리

로 나눕니다.

3 LAYER 아키텍처라고 부릅니다.

 

1 컨드롤러 controller ( 클라이언트의 요청  API/URL 받는것)

 

  • 클라이언트의 요청을 받습니다.
  • 요청에 대한 로직 처리는 Service에게 전담합니다.
    • Request 데이터가 있다면 Service에 같이 전달합니다.
  • Service에서 처리 완료된 결과를 클라이언트에게 응답합니다.

 

2 서비스 service (Functions excluding DB.)

사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세 중에 실세입니다.

  • 따라서 현업에서는 서비스 코드가 계속 비대해지고 있습니다.
  • DB 저장 및 조회가 필요할 때는 Repository에게 요청합니다.

3. repository ( DB 관리 )

 

  • DB 관리 (연결, 해제, 자원 관리) 합니다.
  • DB CRUD 작업을 처리합니다.

전체적으로 보면

 

1. 역할 분리 하기

1. Controller - Service 분리하기

(Controller는 API 요청을 받고 Service에 받아온 데이터와 함께 요청을 넘깁니다.)

memoService 라는 객체를 만들어서 그 안에 함수를 넣고

return 하는 식으로 합니다.

controller 객체.

private final JdbcTemplate jdbcTemplate;

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

@PostMapping("/memos")
public MemoResponseDto createMemo(@RequestBody MemoRequestDto requestDto) {
    memoService memos = new memoService(jdbcTemplate);
    return memos.createMemo(requestDto);
    // RequestDto -> Entity
}

@GetMapping("/memos")
public List<MemoResponseDto> getMemos() {
    memoService memos = new memoService(jdbcTemplate);
    return memos.getMemos();
}

@PutMapping("/memos/{id}")
public Long updateMemo(@PathVariable Long id, @RequestBody MemoRequestDto requestDto) {
 memoService memos = new memoService(jdbcTemplate);
 return memos.putMemo(id,requestDto);
}

@DeleteMapping("/memos/{id}")
public Long deleteMemo(@PathVariable Long id) {
    memoService memos = new memoService(jdbcTemplate);
    return memos.deleteMemo(id);
}

 

memo service 객체

package com.sparta.memo.service;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

public class memoService {
    private final JdbcTemplate jdbcTemplate;

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

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        Memo memo = new Memo(requestDto);

        // DB 저장
        KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체

        String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
        jdbcTemplate.update( con -> {
                    PreparedStatement preparedStatement = con.prepareStatement(sql,
                            Statement.RETURN_GENERATED_KEYS);

                    preparedStatement.setString(1, memo.getUsername());
                    preparedStatement.setString(2, memo.getContents());
                    return preparedStatement;
                },
                keyHolder);

        // DB Insert 후 받아온 기본키 확인
        Long id = keyHolder.getKey().longValue();
        memo.setId(id);

        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);

        return memoResponseDto;
    }

    public List<MemoResponseDto> getMemos() {
        String sql = "SELECT * FROM memo";

        return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
            @Override
            public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                // SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
                Long id = rs.getLong("id");
                String username = rs.getString("username");
                String contents = rs.getString("contents");
                return new MemoResponseDto(id, username, contents);
            }
        });
    }

    public Long putMemo(long id, MemoRequestDto requestDto) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findById(id);
        if(memo != null) {
            // memo 내용 수정
            String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
            jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);

            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
    private Memo findById(Long id) {
        // DB 조회
        String sql = "SELECT * FROM memo WHERE id = ?";

        return jdbcTemplate.query(sql, resultSet -> {
            if(resultSet.next()) {
                Memo memo = new Memo();
                memo.setUsername(resultSet.getString("username"));
                memo.setContents(resultSet.getString("contents"));
                return memo;
            } else {
                return null;
            }
        }, id);
    }

    public Long deleteMemo(long id) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findById(id);
        if(memo != null) {
            // memo 삭제
            String sql = "DELETE FROM memo WHERE id = ?";
            jdbcTemplate.update(sql, id);

            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
}

 

2. service - > repository (새 객체) 

service.java

 

package com.sparta.memo.service;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.memoRepositary.memoRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

public class memoService {
    private final JdbcTemplate jdbcTemplate;

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

    public MemoResponseDto createMemo(MemoRequestDto requestDto) { // create memo service
        Memo memo = new Memo(requestDto);
        // DB 저장
        KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
        memoRepository repo = new memoRepository(jdbcTemplate); // repository
        memo = repo.createkeyHolder(memo,keyHolder);
        // Entity -> ResponseDto
        MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
        return memoResponseDto;
    }

    public List<MemoResponseDto> getMemos() {
        memoRepository repo = new memoRepository(jdbcTemplate); // repository
        return repo.repoGetAllMemo();
    }

    public Long putMemo(long id, MemoRequestDto requestDto) {
        memoRepository repo = new memoRepository(jdbcTemplate); // repository
        return repo.putMemo(requestDto,id);
    }
    public Long deleteMemo(long id) {
        // 해당 메모가 DB에 존재하는지 확인
        memoRepository repo = new memoRepository(jdbcTemplate); // repository
        return repo.deleteMemo(id);
    }
}

repository

package com.sparta.memo.memoRepositary;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.KeyHolder;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;

public class memoRepository {
    private final JdbcTemplate jdbcTemplate;
    public memoRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public Memo createkeyHolder(Memo memo, KeyHolder keyHolder) {
        String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
        jdbcTemplate.update( con -> {
                    PreparedStatement preparedStatement = con.prepareStatement(sql,
                            Statement.RETURN_GENERATED_KEYS);

                    preparedStatement.setString(1, memo.getUsername());
                    preparedStatement.setString(2, memo.getContents());
                    return preparedStatement;
                },
                keyHolder);
        Long id = keyHolder.getKey().longValue();
        memo.setId(id);
        return memo;
    }

    public List<MemoResponseDto> repoGetAllMemo() {
        String sql = "SELECT * FROM memo";
        return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
            @Override
            public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
                // SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
                Long id = rs.getLong("id");
                String username = rs.getString("username");
                String contents = rs.getString("contents");
                return new MemoResponseDto(id, username, contents);
            }
        });
    }

    public Long putMemo(MemoRequestDto requestDto, long id) {
        // memo 내용 수정
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findById(id);
        if(memo != null) {
        String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
        jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
        return id;
        }
        else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }


    ////////////////////////////////////////////////////////////////////////
    private Memo findById(Long id) {
        memoRepository repo = new memoRepository(jdbcTemplate); // repository
        // DB 조회
        String sql = "SELECT * FROM memo WHERE id = ?";
        return jdbcTemplate.query(sql, resultSet -> {
            if(resultSet.next()) {
                Memo memo = new Memo();
                memo.setUsername(resultSet.getString("username"));
                memo.setContents(resultSet.getString("contents"));
                return memo;
            }
            else {
                return null;
            }
        }, id);
    }

    public Long deleteMemo(long id) {
        Memo memo = findById(id);
        if(memo != null) {
            // memo 삭제
            String sql = "DELETE FROM memo WHERE id = ?";
            jdbcTemplate.update(sql, id);

            return id;
        } else {
            throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
        }
    }
}

 

2. LoC 제어의 역전  DI 의존성 주입

IoC, DI는 객체지향의 SOLID 원칙 그리고 GoF 의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴입니다.

 

IoC 는 설계 원칙

DI 는 디자인 패턴

 

e.g 김치 볶음밥 맛있게 만드는 방법

IoC :

  • 신선한 재료를 사용한다.
  • 신 김치를 사용한다.
  • 밥과 김치의 비율을 잘 맞춰야 한다.
  • 볶을 때 재료의 순서가 중요하다.

DI :

  1. 오일을 두른 팬에 채썬 파를 볶아 파기름을 만든다.
  2. 준비한 햄을 넣고 볶다가, 간장 한스푼을 넣어 풍미를 낸다.
  3. 설탕에 버무린 김치를 넣고 함께 볶는다.
  4. 미리 식혀 둔 밥을 넣어 함께 볶는다.
  5. 참기름 한스푼을 넣어 마무리한다.

좋은 코드를 위한 Spring의 IoC와 DI

 

좋은 코드란 무엇일까?

  • 논리가 간단해야 한다.
  • 중복을 제거하고 & 표현을 명확하게 한다.
  • 코드를 처음 보는 사람도 쉽게 이해하고 수정할 수 있어야 한다.
  • 의존성을 최소화.
  • 새로운 기능을 추가 하더라도 크게 구조의 변경이 없어야 한다.
  • ....

따라서 IoC와 DI 는 Spring은 Java를 사용하여 쉽게 좋은 코드를 작성할 수 있도록 도와주는 역할을 해줍니다.

 

‘DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다

 

1. 의존성이란?

 

예를들어 우리가 다리를 다쳐서 목발을 사용하여 걷게 된다면 우리는 걷기 위해 목발에 의존하고 있는 것입니다. 즉, 우리는 목발에 의존성을 두게 되었다고 할 수 있습니다.

코드를 통해 의존성에 대해 이해해보겠습니다.

 

강하게 결합되어있는 Consumer 와 Chicken

void eat() {
    Chicken chicken = new Chicken();
    chicken.eat();
}

public static void main(String[] args) {
    Consumer consumer = new Consumer();
    consumer.eat();
}

커스터머가 먹을걸 바꾸고 깊을때 마다 코드를 수정 해야 한다.

Java 의 Interface 를 활용하면 이를 해결할 수 있습니다

void eat(Food food) {
    food.eat();
}
class Chicken implements Food{
    @Override
    public void eat() {
        System.out.println("치킨을 먹는다.");
    }
}

class Pizza implements Food{
    @Override
    public void eat() {
        System.out.println("피자를 먹는다.");
    }

이처럼 Interface 다형성의 원리를 사용하여 구현하면 고객이 어떠한 음식을 요구하더라도 쉽게 대처할 수 있습니다.

이러한 관계를 약한 결합 및 약한 의존성 이라고 할 수 있습니다.

 

 

 그렇다면 주입은 무엇일까요?

필드에 직접 주입 (variable 들이 public 일 경우)

public static void main(String[] args) {
    Consumer consumer = new Consumer();
    consumer.food = new Chicken();
    consumer.eat();

    consumer.food = new Pizza();
    consumer.eat();
}

 

메서드를 통한 주입 ( variable 들이 private 일 경우 getter/setter 를 사용)

Consumer consumer = new Consumer();
        consumer.setFood(new Chicken());
        consumer.eat();

        consumer.setFood(new Pizza());
        consumer.eat();

 

생성자를 통한 주입 (객체 를 생성할때부터 주입)

Consumer consumer = new Consumer(new Chicken());
        consumer.eat();

consumer = new Consumer(new Pizza());
        consumer.eat();

 

 

2. 메모장에 LoC / DI 

1. 중복 되어 있는 코드를 발견 / 하나로 만듭니다. (중복 되는 Object1 ob1 = new Object1())

2. 강한 결합들을 약하게 만들기.

    2.1 객체의 대한 생성은 딱 한번!

    2.2 객체(음식)을 미리 밖에 만들어서 넣는 방식으로

 

 

3. BEAN ( IoC 컨에이너 와 BEAN )

스프링이 프레임 워크가 객체를 생성해주고 관리 해주는 역할도 아고 있었다.

BEAN 이 바로 스프링이 관리 하고 있는 객체

IoC 컨에이너 는 그 객체들이 모여있는 컨테이너.

 

public class MemoController {

    private final memoService memos ;

    public MemoController(memoService memoservice) {
        this.memos = memoservice;
    }

현제 컨트롤러 안에 있는 (memoService memoservice) 는 빨간줄 오류가 뜹니다.

Spring 이 현제 memoService 라는 타입을 찾을수 없는데 왜 넣으라고 하니? 오류가 뜬겁니다.

 

그러면 memoService 을 어떻게 BEAN 으로 등록을 할가요?

1. memoService  클래스로 들어간다.

2. @Component 를 추가한다.

(Main.java 에 있는 @SpringBootApplication -> @ComponentScan 가 @Component 객체들을 BEAN 으로 등록).

같은 패키지 혹은 하위 패키지 않에 있는 @Component 를 다 찾음.

( BEAN  으로 들록 될때 앞글자가 소문자로 바뀜 MemoCon 이면 memoCon 으로).

 

Spring 은 jdbc 나 개발자들 이 자주 쓰는 lib 들을 이미 BEAN  으로 등록을 해놨어요.

(그래서 JdbcTemplate 를 받을수 있었음)

 

3. 

Autowired 로하는 방법

@Autowired 달기 (Spring 4.3 부터는 안달아도 자동으로 해줍니다/(단 생정자가 한개일때만 해줌)).

 

Lombok 하고 같은거 라고 보시면 됩니다.

 

    3.1 방법1 : Autowired  그냥 필드에다가 (이 방법은 추천하지 않음 / 불안전)

@Autowired
private final JdbcTemplate jdbcTemplate;

    3.2 방법2 : Autowired 생성자 (객체 생성할때) 해주세요 

private final JdbcTemplate jdbcTemplate;
@Autowired
public memoRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;// repository
}

    3.3 방법3 : Autowired 메소드 (Setter 자동으로) 해주세요

private JdbcTemplate jdbcTemplate;
@Autowired
public void setmemoRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;// repository
}

그대신 final 을 제거 해야 합니다, final 은 생성자에 선언이 있어야 합니다.

 

Lombok 으로 하는 방법: @RequiredArgsConstructor

@RequiredArgsConstructor 없을때는 생성자를 만들어야 하지만.

@Component

public class memoService {
    private final memoRepository repo; // repository
    public memoService(memoRepository jdbcTemplate) {
        repo = jdbcTemplate; // repository
    }

@RequiredArgsConstructor 를 넣으면 생성자 필요 x 

@RequiredArgsConstructor
public class memoService {
    private final memoRepository repo; // repository

 

(비슷한게 @resource 하고 @injection)

 

수동으로 IoC 컨데이너에서 Bean 가져오는 방법.

import org.springframework.context.ApplicationContext;

 

1. 생성자에 이름 으로 찾기

public memoService(ApplicationContext applicationContext) {
    memoRepository repo = (memoRepository) applicationContext.getBean("memoRepository");
    this.repo = repo; // repository
}

2. 생성자에 클래스 로 찾기

public memoService(ApplicationContext applicationContext) {
    memoRepository repo = applicationContext.getBean(memoRepository.class);
    this.repo = repo; // repository
}

 

4. @ (Controller, RestController, Service, Repository) 

사실 이 애노테이션 안에는 @Component 가 있었음. 그래서 찾아서 bean 으로 등록이 되었던것.

 

@Controller : Spring 형태를 컨트롤함 (프레젠테이션 레이어, 웹 요청과 응답을 처리함)

@RestController : Json 과 XML 형태를 컨트롤함 (프레젠테이션 레이어, 웹 요청과 응답을 처리함)

@Service : 서비스 레이어, 내부에서 자바 로직을 처리함.

@Repository : 퍼시스턴스 레이어, DB나 파일같은 외부 I/O 작업을 처리함.

 

5. JPA ( ORM ) 

DB를 직접 다룰 때의 문제점.

e.g

아래 객체를 저장 하려면

public class Memo {
    private Long id;
    private String username;
    private String contents;
}

 

DB 테이블은 만들고 저장 해야함.

create table if not exists memo (
                      id bigint not null auto_increment,
                      contents varchar(500) not null,
                      username varchar(255) not null,
                      primary key (id)
);

 

애플리케이션에서 SQL 작성

String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
String sql = "SELECT * FROM memo";

 

이걸또 JDBC 사용해서 직접 실행...

jdbcTemplate.update(sql, "Robbie", "오늘 하루도 화이팅!");
jdbcTemplate.query(sql, ...);

심지어 결과를 객체로 직접 만들어 줘야함...

@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
       // SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
       Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}

 

그리고 SQL 의존적이라 변경에 취약합니다...

....

SQL을 직접 수정해야 합니다......

....

MemoResponseDto 객체에 값을 넣어주는 부분도 당연히 추가해줘야 합니다.

 

이 복잡하고 힘든것들을 그냥 JPA ( 자바 ORM ) 가 해결해 줍니다.

( ORM은 객체와 DB 의 관계를 매핑 해주는 도구입니다. Object-Relational Mapping ).

orm

JPA

 

 

 

 

JPA는 애플리케이션과 JDBC 사이에서 동작되고 있습니다.

JPA를 사용하면 DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해줍니다.

또한 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있습니다.

 

( 하이버네이트(Hibernate)란? )

  • JPA 는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준하이버네이트입니다.
  • 스프링 부트에서는 기본적으로 ‘하이버네이트’ 구현체를 사용 중입니다.

사실상 표준 (de facto, 디팩토) 보통 기업간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준이다