데이터베이스 접근 기술 적용해 보기
1. 프로젝트 세팅
더보기
build.gradle 의존성 추가
- JDBC Template, MySQL 의존성 추가
// MySQL
implementation 'mysql:mysql-connector-java:8.0.28'
// JDBC Template
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
프로젝트 설정
- IntelliJ Database 연동하기
CREATE TABLE memo
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '메모 식별자',
title VARCHAR(100) NOT NULL COMMENT '제목',
contents TEXT COMMENT '내용'
);
2. JDBC Template 적용하기
더보기
DataSource 설정
spring.datasource.url=jdbc:mysql://localhost:3306/memo
spring.datasource.username=계정
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
새로운 Repository를 위한 준비
- 기존 MemoRepositoryImpl 제
3. 메모 생성 API
더보기
새로운 Repository 생성
- Memo Id 자동 생성으로 @Setter 제거
@Getter
@AllArgsConstructor
public class Memo {
private Long id;
private String title;
private String contents;
public Memo(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title, String contents) {
this.title = title;
this.contents = contents;
}
public void update(String title) {
this.title = title;
}
}
- SQL Mapper를 사용하기 위해 saveMemo의 반환타입 수정
- 조회 결과를 객체에 Mapping할 때 MemoResponseDto로 Mapping
public interface MemoRepository {
MemoResponseDto saveMemo(Memo memo);
}
- 새로운 Repository 구현체 생성
@Repository
public class JdbcTemplateMemoRepository implements MemoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemoRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public MemoResponseDto saveMemo(Memo memo) {
}
@Override
public List<MemoResponseDto> findAllMemos() {
return null;
}
@Override
public Memo findMemoById(Long id) {
return null;
}
@Override
public void deleteMemo(Long id) {
}
}
@Override
public MemoResponseDto saveMemo(MemoRequestDto requestDto) {
// 요청받은 데이터로 Memo 객체 생성
Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());
// 저장
return memoRepository.saveMemo(memo);
}
저장 로직 구현하기
@Override
public MemoResponseDto saveMemo(Memo memo) {
// INSERT Query를 직접 작성하지 않아도 된다.
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());
}
@Getter
@AllArgsConstructor
public class MemoResponseDto {
private Long id;
private String title;
private String contents;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.title = memo.getTitle();
this.contents = memo.getContents();
}
}
- 문제점
- MemoResponseDto를 직접 생성하는 것이 불편하다.
4. 메모 목록 조회 API
더보기
리팩토링 및 기능 구현
- JDBC Template을 사용하여 내가 원하는 객체로 변환할 수 있다.
@Repository
public class JdbcTemplateMemoRepository implements MemoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemoRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public List<MemoResponseDto> findAllMemos() {
return jdbcTemplate.query("select * from memo", memoRowMapper());
}
private RowMapper<MemoResponseDto> memoRowMapper() {
return new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
return new MemoResponseDto(
rs.getLong("id"),
rs.getString("title"),
rs.getString("contents")
);
}
};
}
}
5. 메모 단건 조회 API
더보기
리팩토링 및 기능 구현
- List의 경우 데이터가 없으면 빈 배열을 응답했지만 Memo 객체 하나 조회의 경우
null 값을 안전하게 다루기 위해 Optional 사용- null값이 올 수 있는 값을 감싸는 Wrapper 클래스
- NPE(NullPointerException)를 방지한다.
public interface MemoRepository {
Optional<Memo> findMemoById(Long id);
}
@Repository
public class JdbcTemplateMemoRepository implements MemoRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemoRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@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 new RowMapper<Memo>() {
@Override
public Memo mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Memo(
rs.getLong("id"),
rs.getString("title"),
rs.getString("contents")
);
}
};
}
}
@Override
public MemoResponseDto findMemoById(Long id) {
// 식별자의 Memo가 없다면?
Optional<Memo> optionalMemo = memoRepository.findMemoById(id);
// NPE 방지
if (optionalMemo.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
return new MemoResponseDto(optionalMemo.get());
}
- 사이드 이펙트가 발생한 updateMemo( ), updateTitle( ), deleteMemo( ) 로직 임시로 주석처리
@Override
public MemoResponseDto updateMemo(Long id, String title, String contents) {
// // memo 조회
// Memo memo = memoRepository.findMemoById(id);
//
// // NPE 방지
// if (memo == null) {
// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
// }
//
// // 필수값 검증
// if (title == null || contents == null) {
// throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
// }
//
// // memo 수정
// memo.update(title, contents);
//
// return new MemoResponseDto(memo);
return null;
}
@Override
public MemoResponseDto updateTitle(Long id, String title, String contents) {
// // memo 조회
// Memo memo = memoRepository.findMemoById(id);
//
// // NPE 방지
// if (memo == null) {
// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
// }
// // 필수값 검증
// if (title == null || contents != null) {
// throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
// }
//
// memo.update(title);
//
// return new MemoResponseDto(memo);
return null;
}
@Override
public void deleteMemo(Long id) {
// memo 조회
// Memo memo = memoRepository.findMemoById(id);
//
// // NPE 방지
// if (memo == null) {
// throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
// }
//
// memoRepository.deleteMemo(id);
}
6. 메모 전체 수정 API
더보기
리팩토링 및 기능 구현
- 수정된 row 수를 반환받는다.
@Override
public int updateMemo(Long id, String title, String contents) {
// 쿼리의 영향을 받은 row 수를 int로 반환한다.
return jdbcTemplate.update("update memo set title = ?, contents = ? where id = ?", title, contents, id);
}
public interface MemoRepository {
int updateMemo(Long id, String title, String contents);
}
- MemoServiceImpl 로직에서 수정이 실패한다면?
- (수정 성공 → 조회 실패) 데이터 변경은 반영되었으나 응답이 정상적으로 오지 않는다.
- Spring에서 Transaction은 @Transactional를 사용하면 된다.
- 기존 코드는 Repository에 접근하지 않고 메모리에 저장된 memo 객체를 직접 수정했다.
@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.");
}
// memo 수정
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
더보기
리팩토링 및 기능 구현
public interface MemoRepository {
int updateTitle(Long id, String title);
}
@Override
public int updateTitle(Long id, String title) {
return jdbcTemplate.update("update memo set title = ? where id = ?", title, id);
}
@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.");
}
// memo 제목 수정
int updatedRow = memoRepository.updateTitle(id, title);
// 수정된 row가 0개 라면
if (updatedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No data has been modified.");
}
// 수정된 메모 조회
return new MemoResponseDto(memoRepository.findMemoById(id).get());
}
8. 메모 삭제 API
더보기
리팩토링 및 기능 구현
@Override
public int updateMemo(Long id, String title, String contents) {
// 쿼리의 영향을 받은 row 수를 int로 반환한다.
return jdbcTemplate.update("delete memo set title = ?, contents = ? where id = ?", title, contents, id);
}
public interface MemoRepository {
int deleteMemo(Long id);
}
@Override
public void deleteMemo(Long id) {
// memo 삭제
int deletedRow = memoRepository.deleteMemo(id);
// 삭제된 row가 0개 라면
if (deletedRow == 0) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
}
}
9. Optional 잘 사용하기
더보기
MemoRepository 리팩토링
- Optional은 항상 추가적인 검증이 필요하다.
public interface MemoRepository {
Memo findMemoByIdOrElseThrow(Long id);
}
@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));
}
@Service
public class MemoServiceImpl implements MemoService {
private final MemoRepository memoRepository;
public MemoServiceImpl(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
@Override
public MemoResponseDto findMemoById(Long id) {
Memo memo = memoRepository.findMemoByIdOrElseThrow(id);
return new MemoResponseDto(memo);
}
}
해결한 문제점
- 데이터베이스에 영구적으로 데이터가 저장되지 않는다. (Database 접근 기술)
문제점
- 예외 발생 시 공통적으로 처리가 불가능하다.
- 각각의 모든 예외를 try-catch 하여 처리해야 한다.
- RequestDto, ResponseDto를 공유하여 null 값이 들어오기도 한다.
- 필요 없는 필드에 추가적인 null 검사를 해야 한다.
- Spring Bean, 생성자 주입 등 Spring의 동작 원리에 대해 이해하지 못했다.
- 왜 Interface로 만들어서 구현하여 사용하는지 모른다.
'Today I Learned(TIL) > 스파르타 내일배움캠프' 카테고리의 다른 글
주특기 입문/숙련_Day 7 (0) | 2024.12.05 |
---|---|
주특기 입문/숙련_Day 6 (1) | 2024.12.04 |
주특기 입문/숙련_3 Layer Architecture (1) | 2024.12.04 |
주특기 입문/숙련_Day 5 (2) | 2024.12.03 |
주특기 입문/숙련_Day 4 (2) | 2024.12.02 |