메모장 프로젝트 ver3 의 문제점
- 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
- 예외 발생시 공통적으로 처리가 불가능하다.
- 각각의 모든 예외를 try-catch 하여 처리해야 한다.
- RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
- 필요없는 필드에 추가적인 null 검사를 해야한다.
- Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
- 왜 Interface로 만들어서 구현하여 사용하는지 모른다.
📕 목차
- 프로젝트 세팅
- JDBC Template 적용
- 메모 생성 API 리팩토링
- 메모 목록 조회 API 리팩토링
- 메모 단건 조회 API 리팩토링
- 메모 전체 수정 API 리팩토링
- 메모 제목 수정 API 리팩토링
- 메모 삭제 API 리팩토링
- Optional 잘 사용하기
- 해결한 문제점 & 문제점
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 접근 기술)
- 문제점
- 예외 발생시 공통적으로 처리가 불가능하다.
- 각각의 모든 예외를 try-catch 하여 처리해야 한다.
- RequestDto, ResponseDto를 공유하여 null값이 들어오기도 한다.
- 필요없는 필드에 추가적인 null 검사를 해야한다.
- Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
- 왜 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 강의 완강 👏 👏 👏
'Spring > 강의' 카테고리의 다른 글
[📗 스프링 입문] 1. 프로젝트 환경 설정 (1) | 2025.05.07 |
---|---|
[📗 스프링 입문] 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (0) | 2025.05.07 |
[📕 기초 Spring] 6-6. Java와 Database (0) | 2025.05.06 |
[📕 기초 Spring] 6-5. SQL 실습 (0) | 2025.05.06 |
[📕 기초 Spring] 6-4. SQL (Structured Query Language) (0) | 2025.05.06 |