본문 바로가기
Today I Learned(TIL)/스파르타 내일배움캠프

주특기 입문/숙련_데이터베이스 접근 기술 적용

by carrot0911 2024. 12. 4.

데이터베이스 접근 기술 적용해 보기

 

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로 만들어서 구현하여 사용하는지 모른다.