Spring/강의

[📕 기초 Spring] 6-7. 메모장 프로젝트 - ver4 (데이터베이스 접근 기술 적용)

가지코딩 2025. 5. 7. 00:18

메모장 프로젝트 ver3 의 문제점

  1. 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
  2. 예외 발생시 공통적으로 처리가 불가능하다.
    • 각각의 모든 예외를 try-catch 하여 처리해야 한다.
  3. RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
    • 필요없는 필드에 추가적인 null 검사를 해야한다.
  4. Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
  5. 왜 Interface로 만들어서 구현하여 사용하는지 모른다.

📕 목차

  1. 프로젝트 세팅
  2. JDBC Template 적용
  3. 메모 생성 API 리팩토링 
  4. 메모 목록 조회 API 리팩토링
  5. 메모 단건 조회 API 리팩토링
  6. 메모 전체 수정 API 리팩토링
  7. 메모 제목 수정 API 리팩토링
  8. 메모 삭제 API 리팩토링
  9. Optional 잘 사용하기
  10. 해결한 문제점 & 문제점

1. 프로젝트 세팅

build.gradle 의존성 추가

  • JDBC Template, MySQL 의존성 추가
// MySQL
implementation 'mysql:mysql-connector-java:8.0.33'
// JDBC Template
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'

 

 

데이터 베이스 생성

  • memo 스키마 생성
  • memo 테이블 생성
CREATE DATABASE memo;

USE memo;

CREATE TABLE memo
(
    id       BIGINT       AUTO_INCREMENT PRIMARY KEY COMMENT '메모 식별자',
    title    VARCHAR(100) NOT NULL COMMENT '제목',
    contents TEXT COMMENT '내용'
);


2. JDBC Template 적용

DataSource 설정

  • /src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/memo
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

 

 

새로운 Repository 생성

  • 기존 MemoRepositoryImpl.java 제거
  • JdbcTemplateMemoRepository.java 생성
@Repository
public class JdbcTemplateMemoRepository implements MemoRepository {
    private final JdbcTemplate jdbcTemplate;
    
    public JdbcTemplateMemoRepository(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Memo saveMemo(Memo memo) {
        return null;
    }

    @Override
    public List<MemoResponseDto> findAllMemos() {
        return List.of();
    }

    @Override
    public Memo findMemoById(Long id) {
        return null;
    }

    @Override
    public void updateMemo(Long id, Memo memo) {

    }

    @Override
    public void deleteMemo(Long id) {

    }
}

3. 메모 생성 API 리팩토링 

SQL Mapper를 사용하기 위해 saveMemo의 반환타입 수정

  • 조회 결과를 객체에 Mapping 할 때 MemoResponseDto로 Mapping
// MemoRepository.java
public interface MemoRepository {
    MemoResponseDto saveMemo(Memo memo);
	...
}
// MemoServiceImpl.java
@Override
public MemoResponseDto saveMemo(MemoRequestDto requestDto) {
    Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());

    return memoRepository.saveMemo(memo);
}

 

 

저장 로직 구현하기

@Override
public MemoResponseDto saveMemo(Memo memo) {
    SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    jdbcInsert.withTableName("memo").usingGeneratedKeyColumns("id");

    Map<String, Object> parameters = new HashMap<>();
    parameters.put("title", memo.getTitle());
    parameters.put("contents", memo.getContents());

    // 저장 후 생성된 key값을 Number 타입으로 반환하는 메서드
    Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));

    return new MemoResponseDto(key.longValue(), memo.getTitle(), memo.getContents());
}

 

+ MemoResponseDto.java - @AllArgsConstructor 어노테이션 추가

 

 

* 문제점

  • MemoResponseDto를 직접 생성하는것이 불편하다.

4. 메모 목록 조회 API 리팩토링

리팩토링 및 기능 구현

// JdbcTemplateMemoRepository.java
@Override
public List<MemoResponseDto> findAllMemos() {
    return jdbcTemplate.query("select * from memo", memoRowMapper());
}

private RowMapper<MemoResponseDto> memoRowMapper() {
    return (rs, rowNum) -> new MemoResponseDto(
            rs.getLong("id"),
            rs.getString("title"),
            rs.getString("contents")
    );
}

5. 메모 단건 조회 API 리팩토링

리팩토링 및 기능 구현

  • null 값을 안전하게 다루기 위해 Optional 사용
// MemoRepository.java
public interface MemoRepository {
	...
    Optional<Memo> findMemoById(Long id);
}
// JdbcTemplateMemoRepository.java
@Override
public Optional<Memo> findMemoById(Long id) {
    List<Memo> result = jdbcTemplate.query("select * from memo where id = ?", memoRowMapperV2(), id);

    return result.stream().findAny();
}

private RowMapper<Memo> memoRowMapperV2() {
    return (rs, rowNum) -> new Memo(
            rs.getLong("id"),
            rs.getString("title"),
            rs.getString("contents")
    );
}
// MemoServiceImple.java
@Override
public MemoResponseDto findMemoById(Long id) {
    Optional<Memo> optionalMemo = memoRepository.findMemoById(id);

    if (optionalMemo.isEmpty()) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
    }

    return new MemoResponseDto(optionalMemo.get());
}

6. 메모 전체 수정 API 리팩토링

리팩토링 및 기능 구현

  • 수정 성공, 조회 실패 시 응답 비정상 문제 해결
    • 트랜젝션 사용 
    • @Transactional 적용
// MemoRepository.java
public interface MemoRepository {
	...
    int updateMemo(Long id, Memo memo);
}
// JdbcTemplateMemoRepository.java
@Override
public int updateMemo(Long id, Memo memo) {
    // 쿼리의 영향을 받은 row 수를 int로 반환한다.
    return jdbcTemplate.update("update memo set title = ?, contents = ? where id = ?", title, contents, id);
}
// MemoServiceImpl.java
@Transactional
@Override
public MemoResponseDto updateMemo(Long id, String title, String contents) {
    if (title == null || contents == null) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
    }

    int updatedRow = memoRepository.updateMemo(id, title, contents);

    // 수정된 row가 0개라면
    if (updatedRow == 0) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No data has been modified.");
    }

    return new MemoResponseDto(memoRepository.findMemoById(id).get());
}

7. 메모 제목 수정 API 리팩토링

리팩토링 및 기능 구현

// MemoRepository.java
public interface MemoRepository {
	...
    int updateTitle(Long id, String title);
}
// JdbcTemplateMemoRepository.java
@Override
public int updateTitle(Long id, String title) {
    return jdbcTemplate.update("update memo set title = ? where id = ?", title, id);
}
// MemoServiceImpl.java
@Transactional
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
    if (title == null || contents != null) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
    }

    int updatedRow = memoRepository.updateTitle(id, title);
    if (updatedRow == 0) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No data has been modified.");
    }

    return new MemoResponseDto(memoRepository.findMemoById(id).get());
}

8. 메모 삭제 API 리팩토링

리팩토링 및 기능 구현

// MemoRepository.java
public interface MemoRepository {
	...
    int deleteMemo(Long id);
}
// JdbcTemplateMemoRepository.java
@Override
public int deleteMemo(Long id) {
    return jdbcTemplate.update("delete from memo where id = ?", id);
}
// MemoServiceImpl.java
@Override
public void deleteMemo(Long id) {
    int deletedRow = memoRepository.deleteMemo(id);

    if (deletedRow == 0) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
    }
}

9. Optional 잘 사용하기

MemoRepository 리팩토링

  • Optional은 항상 추가적인 검증이 필요하다.
  • findMemoById() → findMemoByIdOrElseThrow() 변경
  • service 코드도 수정
// MemoRepository.java
public interface MemoRepository {
    ...
    Memo findMemoByIdOrElseThrow(Long id);
}
// JdbcTemplateMemoRepository.java
@Override
public Memo findMemoByIdOrElseThrow(Long id) {
    List<Memo> result = jdbcTemplate.query("select * from memo where id = ?", memoRowMapperV2(), id);

    return result.stream().findAny().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id));
}
// MemoServiceImpl.java
@Override
public MemoResponseDto findMemoById(Long id) {
    Memo optionalMemo = memoRepository.findMemoByIdOrElseThrow(id);

    return new MemoResponseDto(optionalMemo);
}

10. 해결한 문제점 & 문제점

  • 해결한 문제점
    • 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
  • 문제점
    1. 예외 발생시 공통적으로 처리가 불가능하다.
      • 각각의 모든 예외를 try-catch 하여 처리해야 한다.
    2. RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
      • 필요없는 필드에 추가적인 null 검사를 해야한다.
    3. Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
    4. 왜 Interface로 만들어서 구현하여 사용하는지 모른다.

실습 코드

https://github.com/gajicoding/spring-crud-memo/tree/v1.4.0

 

GitHub - gajicoding/spring-crud-memo

Contribute to gajicoding/spring-crud-memo development by creating an account on GitHub.

github.com


👏 👏 👏  기초 Spring 강의 완강  👏 👏 👏