24.05.16 목 TIL(Today I Learned)

신민금·2024년 5월 17일
0
post-thumbnail

TIL(Today I Learned)

: 매일 저녁, 하루를 마무리하며 작성 !
: ⭕ 지식 위주, 학습한 것을 노트 정리한다고 생각하고 작성하면서 머리 속 흩어져있는 지식들을 정리 !


알고리즘 코드카타

  • 문제 설명
    array의 각 element 중 divisor로 나누어 떨어지는 값을 오름차순으로 정렬한 배열을 반환하는 함수, solution을 작성해주세요.
    divisor로 나누어 떨어지는 element가 하나도 없다면 배열에 -1을 담아 반환하세요.
  • 제한사항
    arr은 자연수를 담은 배열입니다.
    정수 i, j에 대해 i ≠ j 이면 arr[i] ≠ arr[j] 입니다.
    divisor는 자연수입니다.
    array는 길이 1 이상인 배열입니다.
import java.util.*;

class Solution {
    public int[] solution(int[] arr, int divisor) {
        
        int[] answer = {};
        ArrayList<Integer> list = new ArrayList<Integer>();
        
        for(int i=0; i<arr.length; i++) {
              if(arr[i] % divisor==0) list.add(arr[i]);
          }
        
        if(list.isEmpty()) {
              list.add(-1);
          }
        
        answer = new int[list.size()];
          
          for(int i=0; i<list.size(); i++) {
              answer[i] = list.get(i);
          }
          
          Arrays.sort(answer);
        
        return answer;
    }
}

Spring 입문주차 2주차

2-1 3 Layer Architecture

  1. 메모장 프로젝트의 문제점
  • 현재 우리가 작성한 메모장 프로젝트는 Controller 클래스 하나로 모든 API를 처리
  • 현재는 API 수가 적고 기능이 단순하여 코드가 복잡해 보이지 않을 수 있지만 앞으로 기능이 추가되고 복잡해진다면 문제가 발생 ( 한 개의 클래스에 너무 많은 양의 코드가 존재, 현업에서는 코드의 추가 혹은 변경 요청 발생 가능, 문제가 발생했는데 해당 Controller 클래스를 구현한 개발자가 퇴사? )
  1. Spring의 3 Layer Architecture
  • Controller
    1) 클라이언트의 요청을 받음
    2) 요청에 대한 로직 처리는 Service에게 전담, Request 데이터가 있다면 Service에 같이 전달
    3) Service에서 처리 완료된 결과를 클라이언트에게 응답
  • Service
    1) 사용자의 요구사항을 처리 ('비즈니스 로직') 하는 실세
    2) DB 저장 및 조회가 필요할 때는 Repository에게 요청

  • Repository
    1) DB 관리 (연결, 해제, 자원 관리)
    2) DB CRUD 작업을 처리

2-2 역할 분리하기

  1. Controller - Service 분리하기
// Controller - Service 분리 : Controller

package com.sparta.memo.controller;

import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.service.MemoService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api")
public class MemoController {

    private final JdbcTemplate jdbcTemplate;

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

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

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

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

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


// Controller - Service 분리 : 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) {
        // RequestDto -> Entity
        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() {
        // DB 조회
        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 updateMemo(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("선택한 메모는 존재하지 않습니다.");
        }
    }

    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("선택한 메모는 존재하지 않습니다.");
        }
    }

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

  1. Service - Repository 분리하기
// Service - Repository 분리 : Repository

package com.sparta.memo.repository;

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 MemoRepository {

    private final JdbcTemplate jdbcTemplate;

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

    public Memo save(Memo memo) {
        // 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);

        return memo;
    }

    public List<MemoResponseDto> findAll() {
        // DB 조회
        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 void update(Long id, MemoRequestDto requestDto) {
        String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
        jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
    }

    public void delete(Long id) {
        String sql = "DELETE FROM memo WHERE id = ?";
        jdbcTemplate.update(sql, id);
    }

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


// Service - Repository 분리 : 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 com.sparta.memo.repository.MemoRepository;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.List;

public class MemoService {

    private final JdbcTemplate jdbcTemplate;

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

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);

        // DB 저장
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        Memo saveMemo = memoRepository.save(memo);

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

        return memoResponseDto;
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        return memoRepository.findAll();
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id);
        if (memo != null) {
            // memo 내용 수정
            memoRepository.update(id, requestDto);

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

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = memoRepository.findById(id);
        if (memo != null) {
            // memo 삭제
            memoRepository.delete(id);

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


2-3 IoC(제어의 역전), DI(의존성 주입) 이해하기

  1. Spring의 IoC와 DI : IoC, DI는 객체지향의 SOLID 원칙 그리고 GoF 의 디자인 패턴과 같은 설계 원칙 및 디자인 패턴
  1. 의존성이란?
// 강하게 결합되어있는 Consumer 와 Chicken

public class Consumer {

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

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

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

// 코드가 실행되는데 있어서는 아무런 문제가 없지만 만약 Consumer 가 치킨이 아니라 피자를 먹고 싶어 한다면? 
// 많은 수의 코드 변경이 불가피, Java 의 Interface 를 활용하면 결합을 약하게 해결

public class Consumer {

    void eat(Food food) {
        food.eat();
    }

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

interface Food {
    void eat();
}

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

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

// 약한 결합 및 약한 의존성
  1. 주입이란?
// 필드에 직접 주입

public class Consumer {

    Food food;

    void eat() {
        this.food.eat();
    }

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

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

interface Food {
    void eat();
}

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

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


// 메서드를 통한 주입

public class Consumer {

    Food food;

    void eat() {
        this.food.eat();
    }

    public void setFood(Food food) {
        this.food = food;
    }

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

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

interface Food {
    void eat();
}

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

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


// 생성자를 통한 주입

public class Consumer {

    Food food;

    public Consumer(Food food) {
        this.food = food;
    }

    void eat() {
        this.food.eat();
    }

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

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

interface Food {
    void eat();
}

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

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


  1. 제어의 역전이란?

2-4 메모장 프로젝트의 IoC & DI

  1. 객체 중복 생성 문제 해결
    MemoService를 보면 "new MemoRepository(jdbcTemplate);" 코드가 중복
public class MemoService {

    private final JdbcTemplate jdbcTemplate;

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

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // DB 저장
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
				...
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }

    public Long deleteMemo(Long id) {
        MemoRepository memoRepository = new MemoRepository(jdbcTemplate);
        ...
    }
}

MemoService가 생성될 때, MemoRepository를 딱 한 번 생성해서 계속 사용

public class MemoService {
   
		// 멤버 변수 선언
    private final MemoRepository memoRepository;

    // 생성자: MemoService(JdbcTemplate jdbcTemplate)가 생성될 때 호출됨
		public MemoService(JdbcTemplate jdbcTemplate) {
				// 멤버 변수 생성
        this.memoRepository = new MemoRepository(jdbcTemplate);
    }
    
   public MemoResponseDto createMemo(MemoRequestDto requestDto) {
				// RequestDto -> Entity
        Memo memo = new Memo(requestDto);

        // DB 저장
        Memo saveMemo = memoRepository.save(requestDto);

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

        return memoResponseDto;
    }
	...
}
  1. 강한 결합 상태의 메모장 프로젝트
  • "강한 결합"의 문제점

  • 강한 결합 해결방법
    1) Repository1 클래스 선언 및 객체 생성 → repository1

public class Repository1 { ... }

// 객체 생성
Repository1 repository1 = new Repository1();

2) Service1 클래스 선언 및 객체 생성 (repostiroy1 사용) → service1

Class Service1 {
	private final Repository1 repitory1;

	// repository1 객체 사용
	public Service1(Repository1 repository1) {
		this.repository1 = new Repository1();
		this.repository1 = repository1;
	}
}

// 객체 생성
Service1 service1 = new Service1(repository1);

3) Contoller1 선언 ( service1 사용)


Class Controller1 {
	private final Service1 service1;

	// service1 객체 사용
	public Controller1(Service1 service1) {
		this.service1 = new Service1();
		this.service1 = service1;
	}
}

4) Repository1 객체 생성 시 DB 접속 id, pw 를 받아서 DB 접속 시 사용, 생성자에 DB 접속 id, pw 를 추가

public class Repository1 {

	public Repository1(String id, String pw) {
    // DB 연결
    Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/memo", id, pw);
  }
}

// 객체 생성
String id = "root";
String pw = "1234";
Repository1 repository1 = new Repository1(id, pw);
  • 개선 결과 : Repository1 생성자 변경은 이제 누구에게도 피해(?) 를 주지 않음, Service1 생성자가 변경되면? 모든 Contoller → Controller 변경 필요 X
  1. 제어의 역전

    "제어의 역전 (IoC: Inversion of Control)"
    프로그램의 제어 흐름이 뒤바뀜
  • 강한 결합 상태의 메모장 프로젝트는 비효율적인 코드 구성 ( 제어의 흐름은 Controller ⇒ Service ⇒ Repository )

  • DI 즉, 의존성 주입을 통해 제어의 흐름을 Repository ⇒ Service ⇒ Controller 로 역전함으로써 효율적인 코드

2-5 IoC Container와 Bean

  1. Spring IoC 컨테이너
  • 빈 (Bean) : Spring이 관리하는 객체
  • Spring IoC 컨테이너 : 'Bean'을 모아둔 컨테이너
  1. Spring 'Bean' 등록 방법
// @Component : ‘Bean’으로 등록하고자하는 클래스 위에 설정

@Component
public class MemoService { ... }


//@Component가 설정된 클래스에 대해서 Spring이 해 주는 일 확인하기

// 1. MemoService 객체 생성
MemoService memoService = new MemoService();

// 2. Spring IoC 컨테이너에 Bean (memoService) 저장
// memoService -> Spring IoC 컨테이너


// @ComponentScan : Spring 서버가 뜰 때 @ComponentScan에 
// 설정해 준 packages 위치와 하위 packages 들을 전부 확인하여 
// @Component가 설정된 클래스들을 ‘Bean’으로 등록

@Configuration
@ComponentScan(basePackages = "com.sparta.memo")
class BeanConfig { ... }

  1. Spring 'Bean' 사용 방법
    1) @Autowired
// 필드 위에 @Autowired

@Component
public class MemoService {
		
    @Autowired
    private MemoRepository memoRepository;
		
		// ...
}


// 'Bean'을 주입할 때 사용할 메서드 위에 @Autowired

@Component
public class MemoService {

    private final MemoRepository memoRepository;

    @Autowired
    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }
		
		// ...
}


// @Autowired 적용 조건 : 
// Spring IoC 컨테이너에 의해 관리되는 클래스에서만 가능 
// ( Spring IoC 컨테이너에 의해 관리되는 ‘Bean’객체만 DI에 사용 )


// @Autowired 생략 조건 : 생성자 선언이 1개 일 때만 생략 가능


// Lombok 의 @RequiredArgsConstructor를 사용하면 다음과 같이 코딩 가능

@Component
@RequiredArgsConstructor // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {

    private final MemoRepository memoRepository;
    
//    public MemoService(MemoRepository memoRepository) {
//        this.memoRepository = memoRepository;
//    }

		...

}

2) ApplicationContext

// ApplicationContext는 BeanFactory등을 상속하여 기능을 확장한 Container
// BeanFactory는 ‘Bean’ 의 생성, 관계설정등의 제어를 담당하는 IoC 객체

@Component
public class MemoService {

		private final MemoRepository memoRepository;

    public MemoService(ApplicationContext context) {
        // 1.'Bean' 이름으로 가져오기
        MemoRepository memoRepository = (MemoRepository) context.getBean("memoRepository");

        // 2.'Bean' 클래스 형식으로 가져오기
        // MemoRepository memoRepository = context.getBean(MemoRepository.class);

        this.memoRepository = memoRepository;
    }

		...		
}

  1. 3 Layer Annotation

2-6 JPA란 무엇일까?

  1. DB를 직접 다룰 때의 문제점
  • SQL 의존적이라 변경에 취약
public class Memo {
    private Long id;
    private String username;
    private String contents;
		
		// 비밀번호 추가
		private String password;
}


// 1. SQL을 직접 수정

// 2. MemoResponseDto 객체에 값을 넣어주는 부분도 당연히 추가
  1. ORM이란?
  • ORM : Object-Relational Mapping, 객체와 DB의 관계를 매핑 해주는 도구
  • Object : "객체"지향 언어 (자바, 파이썬)
  • Relational : "관계형" 데이터베이스 (H2, MySQL)
  1. JPA란?
  • JPA : Java Persistence API, 자바 ORM 기술에 대한 표준 명세

  • 하이버네이트(Hibernate) : JPA 는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준

2-7 Entity 이해하기

  1. Entity란?
  • JPA에서 관리되는 클래스 즉, 객체를 의미
  • Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리
  1. 프로젝트 생성
// src > main > resources > META-INF > persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="memo">
        <class>com.sparta.entity.Memo</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>

            <property name="hibernate.hbm2ddl.auto" value="create" />

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>


// build.gradle

// JPA 구현체인 hibernate
implementation 'org.hibernate:hibernate-core:6.1.7.Final'
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'


// Intellij Database 연동

CREATE DATABASE memo;


// EntityTest

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class EntityTest {

    EntityManagerFactory emf;
    EntityManager em;

    @BeforeEach
    void setUp() {
        emf = Persistence.createEntityManagerFactory("memo");
        em = emf.createEntityManager();
    }

    @Test
    void test1() {

    }
}


  1. Entity 클래스 만들기
// Memo Entity

package com.sparta.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
    @Id
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 길이 지정
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}


// @GeneratedValue 옵션을 추가하면 기본 키 생성을 DB에 위임

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
    @Id
		@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 길이 지정
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}

2-8 영속성 컨텍스트란 무엇일까?

  1. 영속성 컨텍스트란? : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간
  • 개발자들은 이제 직접 SQL을 작성하지 않아도 JPA를 사용하여 DB에 데이터를 저장하거나 조회할 수 있으며 수정, 삭제 또한 가능

  • 일련의 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통

  1. EntityManager
  • 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요
  • 개발자들은 EntityManager를 사용해서 Entity를 저장하고 조회하고 수정하고 삭제
  • EntityManager는 EntityManagerFactory를 통해 생성하여 사용
  1. EntityManagerFactory

  1. JPA의 트랜잭션
  • 트랜잭션은 DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념
  • 가장 큰 특징은 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있다는 점
  • 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌린다.
START TRANSACTION; # 트랜잭션을 시작합니다.

INSERT INTO memo (id, username, contents) VALUES (1, 'Robbie', 'Robbie Memo');
INSERT INTO memo (id, username, contents) VALUES (2, 'Robbert', 'Robbert Memo');
SELECT * FROM memo;

COMMIT; # 트랜잭션을 커밋합니다.

SELECT * FROM memo;
  1. 영속성 컨텍스트 Debugging으로 확인하기

2-9 영속성 컨텍스트의 기능

  1. 1차 캐시
  • 영속성 컨텍스트는 내부적으로 캐시 저장소 존재, 저장하는 Entity 객체들이 1차 캐시 즉, 캐시 저장소에 저장된다고 생각
  • 캐시 저장소는 Map 자료구조 형태
  • key에는 @Id로 매핑한 기본 키 즉, 식별자 값을 저장
  • value에는 해당 Entity 클래스의 객체를 저장
  • 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자값을 사용하여 Entity 객체를 구분하고 관리
  • Entity 저장
  • Entity 조회
    1) 캐시 저장소에 조회하는 Id가 존재하지 않은 경우 ( 캐시 저장소 조회 -> DB SELECT 조회 후 캐시 저장소에 저장 )

2) 캐시 저장소에 조회하는 Id가 존재하는 경우

  • Entity 삭제
  1. 쓰기 지연 저장소(ActionQueue)
// 쓰기 지연 저장소 (ActionQueue)

@Test
@DisplayName("쓰기 지연 저장소 확인")
void test6() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(2L);
        memo.setUsername("Robbert");
        memo.setContents("쓰기 지연 저장소");
        em.persist(memo);

        Memo memo2 = new Memo();
        memo2.setId(3L);
        memo2.setUsername("Bob");
        memo2.setContents("과연 저장을 잘 하고 있을까?");
        em.persist(memo2);

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

// flush() 

@Test
@DisplayName("flush() 메서드 확인")
void test7() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        Memo memo = new Memo();
        memo.setId(4L);
        memo.setUsername("Flush");
        memo.setContents("Flush() 메서드 호출");
        em.persist(memo);

        System.out.println("flush() 전");
        em.flush(); // flush() 직접 호출
        System.out.println("flush() 후\n");
        

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}


  1. 변경 감지(Dirty Checking)
// 변경할 memo 데이터를 조회한 후 변경

@Test
@DisplayName("변경 감지 확인")
void test8() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        System.out.println("변경할 데이터를 조회합니다.");
        Memo memo = em.find(Memo.class, 4);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("\n수정을 진행합니다.");
        memo.setUsername("Update");
        memo.setContents("변경 감지 확인");

        System.out.println("트랜잭션 commit 전");
        et.commit();
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}

2-10 Entity의 상태

  1. 비영속(Transient)
  • new 연산자를 통해 인스턴스화 된 Entity 객체를 의미
  • 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 X
Memo memo = new Memo(); // 비영속 상태
memo.setId(1L);
memo.setUsername("Robbie");
memo.setContents("비영속과 영속 상태");
  1. 영속(Managed)
  • persist(entity) : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만든다. : em.persist(memo);
@Test
@DisplayName("비영속과 영속 상태")
void test1() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {

        Memo memo = new Memo(); // 비영속 상태
        memo.setId(1L);
        memo.setUsername("Robbie");
        memo.setContents("비영속과 영속 상태");

        em.persist(memo);

        et.commit();

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}
  1. 준영속(Detached)
  • detach(entity) : em.detach(memo); - 특정 Entity만 준영속 상태로 전환
  • clear() : em.clear(); - 영속성 컨텍스트를 완전히 초기화
  • close() : em.close(); - 영속성 컨텍스트를 종료
  • 준영속 상태에서 다시 영속 상태로 바꾸는 방법 : merge(entity) - 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
  1. 삭제(Removed) : em.remove(memo); - 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환

2-11 SpringBoot의 JPA

  1. SpringBoot 환경에서의 JPA
@PersistenceContext
EntityManager em;

@PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용

  1. Spring의 트랜잭션
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
						...
			
		@Transactional
		@Override
		public <S extends T> S save(S entity) {
		
			Assert.notNull(entity, "Entity must not be null");
		
			if (entityInformation.isNew(entity)) {
				em.persist(entity);
				return entity;
			} else {
				return em.merge(entity);
			}
		}

						...
}

Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공

  1. @Transactional
// 메모 생성 성공

@Test
@Transactional 
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
    Memo memo = new Memo();
    memo.setUsername("Robbert");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}


// 메모 생성 실패

@Test
@DisplayName("메모 생성 실패")
void test2() {
    Memo memo = new Memo();
    memo.setUsername("Robbie");
    memo.setContents("@Transactional 테스트 중!");

    em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
  1. 영속성 컨텍스트와 트랜잭션의 생명주기
  • 스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치
  • 트랜잭션이 유지되는 동안은 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능을 사용 가능
  • 앞에서 작성한 테스트 코드 메서드에 트랜잭션이 적용되지 않았기 때문에 영속성 컨텍스트가 유지되지 못해 오류가 발생
  1. 트랜잭션 전파
  • 실행 결과 자식 메서드 createMemo가 종료될 때 update가 실행되는 것이 아니라 부모 메서드에 트랜잭션이 합류되면서 부모 메서드가 종료된 후 트랜잭션이 커밋될 때 update가 실행된 것을 확인
  • 자식 메서드가 종료된 후 트랜잭션이 커밋되면서 update가 실행된 것을 확인

2-12 Spring Data JPA란 무엇일까?

  1. Spring Data JPA란?
  • Spring Data JPA는 JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈 ( JPA를 추상화시킨 Repository 인터페이스를 제공 )
  • Repository 인터페이스는 Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용 ( 개발자들은 Repository 인터페이스를 통해 JPA를 간편하게 사용 )
  1. Spring Data JPA의 SimpleJpaRepository
  • Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성 ( - Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면,
  • 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring ‘Bean’으로 등록 )
  • 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용
  1. Spring Data JPA 사용방법
  • JpaRepository 등록
  1. 메모장 프로젝트 Spring Data JPA 적용
// MemoRepository

public interface MemoRepository extends JpaRepository<Memo, Long> {

}

// MemoService

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.repository.MemoRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class MemoService {

    private final MemoRepository memoRepository;

    public MemoService(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

    public MemoResponseDto createMemo(MemoRequestDto requestDto) {
        // RequestDto -> Entity
        Memo memo = new Memo(requestDto);

        // DB 저장
        Memo saveMemo = memoRepository.save(memo);

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

        return memoResponseDto;
    }

    public List<MemoResponseDto> getMemos() {
        // DB 조회
        return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
    }

    @Transactional
    public Long updateMemo(Long id, MemoRequestDto requestDto) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findMemo(id);

        // memo 내용 수정
        memo.update(requestDto);

        return id;
    }

    public Long deleteMemo(Long id) {
        // 해당 메모가 DB에 존재하는지 확인
        Memo memo = findMemo(id);

        // memo 삭제
        memoRepository.delete(memo);

        return id;
    }

    private Memo findMemo(Long id) {
        return memoRepository.findById(id).orElseThrow(() ->
                new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
        );
    }
}

2-13 JPA Auditing 적용하기

  1. Timestamped
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}

  1. 메모장 프로젝트에 JPA Auditing 적용
// entity > Timestamped

package com.sparta.memo.entity;

import jakarta.persistence.*;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {

    @CreatedDate
    @Column(updatable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt;
}

2-14 Query Methods란 무엇일까?

  1. Query Methods란? : Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공
// MemoRepository

package com.sparta.memo.repository;

import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc();
}

  1. 메모장 프로젝트 Query Methods 적용
// 최신 메모가 가장 상단에 나올 수 있도록 수정

public List<MemoResponseDto> getMemos() {
    // DB 조회
    return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}

개인 프로젝트 진행

profile
야옹

0개의 댓글