기록하며 성장한다 - 개발, 회고

개발 회고록

[ 개발 회고록 ] JPA nativeQuery와 SpringBoot를 이용한 CRUD 개발

전대홍 2023. 11. 21. 23:17

 

 

해당 포스팅은 JPA native Query와 Spring Boot를 이용한 CRUD 개발 1편이다.

F-lab 멘토링 과제를 위한 개발을 진행중이며,

멘토님께 피드백을 받으며 개발을 할 때 신경써야 하는 부분을 찾아가고 있다.

 

간단한 CRUD를 개발해 보기로 했다.

퇴사를 한 이후, 이론 공부도 좋지만 개발의 감을 잃지 않기 위해서도 있고, F-lab를 하는 중 멘토님이 만들어보라는 과제를 주셨기 때문이다.

다만 걱정인 부분은 이전에 다니던 회사에서 개발하던 방식이 손에 익어있는데, 그 방식이 너무 오래된 개발 방식이라는 점이다. 그래서 개발을 하면서 중간중간 피드백도 받고, 그에 따라 수정하는 식으로 진행하려 한다.

 

참고로 해당 글은 CRUD 개발을 알려주는 글이 아닌, 필자가 개발을 하고 그 코드 중 중요한 부분을 수정하고, 이슈를 찾아서 해결하는 과정 등을 회고록으로 남긴 것이므로, CRUD 개발 방법을 공부하기 위해 해당 포스팅을 들어온 분이라면 조금 부족한 글이 될 수 있음을 유의하길 바랍니다.

 


개발 중 일어난 이슈 해결

1. 깃허브에 properties 민감 정보 올라가지 않게 하는 방법

2. 멀티스레드 테스트 중 오류 발생 해결

 

 

시작

우선 처음에는 SpringBoot로 프로젝트를 만들었다.

Java는 11, 스프링부트는 2.7.17 버전을 선택하였다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation group: 'org.springframework.security', name: 'spring-security-crypto', version: '5.6.12'
}

gradle의 Dependency는 위와 같이 설정하였다.

mybatis와 JPA중 아무거나 선택을 해도 된다고 했지만, JPA를 사용할 경우 native Query를 사용하라고 하였고, JPA와 조금은 친숙해지기 위해 필자는 JPA를 선택하였다. 그리고 개발에 도움을 주는 lombok과, Mysql 연결을 위한 dependency도 추가하였다.

다음으로 한 건 mysql 연결이었다.

우선 application.properties 를 만든 후,

# mysql
spring.profiles.include = aws

이렇게 include를 해주고, application-aws.properties 에 민감한 mysql 정보를 입력해주었다.

 


회원 관련 개발

회원과 관련된 기능은 회원가입, 회원정보수정, 회원탈퇴, 로그인, 로그아웃 이렇게 5가지를 개발하였다.

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    /**
     * 회원가입
     */
    @Modifying
    @Query( value = "INSERT INTO TB_USER (LOGIN_ID, USER_NM, PWSD, CRTE_DTTM, UPDT_DTTM)" +
                    "VALUES (:loginId, :userName, :password, :createDateTime, :updateDatetime)",
            nativeQuery = true)
    void joinUser(@Param(value = "loginId") String loginId,
                  @Param(value = "userName") String userName,
                  @Param(value = "password") String password,
                  @Param(value = "createDateTime") LocalDateTime createDateTime,
                  @Param(value = "updateDatetime") LocalDateTime updateDatetime);


    /**
     * 중복 아이디 확인
     */
    @Query ( value = "SELECT * FROM TB_USER WHERE LOGIN_ID = :loginId", nativeQuery = true )
    User overlabCheck(@Param(value = "loginId") String loginId);


    /**
     * 내 정보 가져오기
     */
    @Query ( value = "SELECT * FROM TB_USER WHERE USER_ID = :userId", nativeQuery = true )
    User getMyInfo(@Param(value = "userId") Long userId);


    /**
     * 회원 정보 수정
     */
    @Transactional
    @Modifying
    @Query( value = "UPDATE TB_USER SET USER_NM = :userName, UPDT_DTTM = :updateDatetime WHERE USER_ID = :userId", nativeQuery = true)
    void updateUser(@Param(value = "userId") Long userId,
                    @Param(value = "userName") String userName,
                    @Param(value = "updateDatetime") LocalDateTime updateDatetime);

    /**
     * 회원 탈퇴
     */
    @Modifying
    @Query( value = "DELETE FROM TB_USER WHERE USER_ID = :userId", nativeQuery = true)
    void deleteUser(@Param(value = "userId") Long userId);

}

 

원래 JPA를 쓴다면, save나 findById 와 같은 메서드를 사용하여 쉽게 구현할 수 있겠지만 native query를 사용해야 했기 때문에, 위 Code 처럼 Repository를 작성하였다.

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    /**
     * 회원가입
     */
    @Override
    public String joinUser(String loginId, String userName, String password) {

        if ( password == null ) return ResponseCode.ERROR_CODE;

        User overlabCheckUser = userRepository.overlabCheck(loginId);
        if ( overlabCheckUser != null ) return ResponseCode.OVERLAB_ERROR_CODE;

        try {
            String bCryptPassword = bCryptPasswordEncoder.encode(password);

            userRepository.joinUser(
                    loginId,
                    userName,
                    bCryptPassword,
                    LocalDateTime.now(),
                    LocalDateTime.now());

            return ResponseCode.SUCCESS_CODE;

        } catch ( Exception e ) {

            e.printStackTrace();
            return ResponseCode.ERROR_CODE;

        }
    };


    /**
     * 로그인
     */
    @Override
    public User login(String loginId, String password) {
        User dbUser = userRepository.overlabCheck(loginId);
        if ( dbUser != null ) {
            boolean passwordCheck = bCryptPasswordEncoder.matches(password, dbUser.getPassword());
            if (!passwordCheck) dbUser = null;
        }
        return dbUser;
    }


    /**
     * 내 정보 가져오기
     */
    @Override
    public User getMyInfo(Long userId) {
        return userRepository.getMyInfo(userId);
    }


    /**
     * 회원정보 수정
     */
    @Override
    public String updateUser(Long userId, String loginId, String userName) {
        User dbUser = userRepository.overlabCheck(loginId);
        if ( dbUser == null ) return ResponseCode.ERROR_CODE;
        try {
            userRepository.updateUser(userId, userName, LocalDateTime.now());
            return ResponseCode.SUCCESS_CODE;
        } catch ( Exception e ) {
            e.printStackTrace();
            return ResponseCode.ERROR_CODE;
        }
    }

    /**
     * 회원 탈퇴
     */
    @Override
    public String deleteUser(Long userId, String loginId) {
        User dbUser = userRepository.overlabCheck(loginId);
        if ( dbUser == null ) return ResponseCode.ERROR_CODE;
        try {
            userRepository.deleteUser(userId);
            return ResponseCode.SUCCESS_CODE;
        } catch ( Exception e ) {
            e.printStackTrace();
            return ResponseCode.ERROR_CODE;
        }
    }
}

다음은 Service를 만들었다. Service는 interface와 implements로 나눠 개발을 진행했다.

회원가입 시에는 중복된 아이디가 있는지 체크를 하였고, 비밀번호는 BCryptPasswordEncoder를 활용해서 암호화를 진행하였다. BCryptPasswordEncoder를 사용하기 위해서는 Config에 Bean으로 등록을 해야하는데

/**
 * BCryptPasswordEncoder Config
 */
@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

이렇게 간단하게 등록해두었다.

@RestController
@RequiredArgsConstructor
@Slf4j
public class UserController {

    private final UserService userService;
    private final BoardService boardService;

    /*
     * Validation Check Overload Method
     */
    public boolean validationCheck(String loginId, String userName, String password) {
        if (loginId.equals("") || userName.equals("") || password.equals("")) return false;
        return true;
    }
    public boolean validationCheck(String loginId, String userName) {
        if (loginId.equals("") || userName.equals("")) return false;
        return true;
    }


    /**
     *  회원가입
     */
    @PostMapping("/user/join")
    public String joinUser(@RequestParam(value = "loginId") String loginId,
                           @RequestParam(value = "userName") String userName,
                           @RequestParam(value = "password") String password) {

        if ( ! validationCheck(loginId, userName, password) ) return ResponseCode.ERROR_CODE;

        return userService.joinUser(loginId, userName, password);
    }

    /**
     *  내 정보 수정하기
     */
    @PostMapping("/user/update")
    public String updateUser(@RequestParam(value = "userId") Long userId,
                             @RequestParam(value = "loginId") String loginId,
                             @RequestParam(value = "userName") String userName,
                             HttpServletRequest request, HttpSession session) {

        // Session 끊킬 시 redirect
        User sessionUser = (User) session.getAttribute("loginUser");
        boolean loginSession = sessionUser != null;
        if ( !loginSession ) return "redirect:/loginPage";

        if ( ! validationCheck(loginId, userName) ) return ResponseCode.ERROR_CODE;

        String resultCode = userService.updateUser(userId, loginId, userName);

        if ( resultCode.equals(ResponseCode.SUCCESS_CODE)) {
            sessionUser.setUserName(userName);
            session.setAttribute("loginUser", sessionUser);
        }
        return resultCode;
    }

    /**
     *  회원 탈퇴
     */
    @PostMapping("/user/delete")
    public String updateUser(@RequestParam(value = "loginId") String loginId,
                             @RequestParam(value = "userId") Long userId,
                             HttpSession session) {

        // Session 끊킬 시 redirect
        User user = (User) session.getAttribute("loginUser");
        boolean loginSession = user != null;
        if ( !loginSession ) return "redirect:/loginPage";

        boardService.deleteBoardAllByUser(userId);
        String resultCode = userService.deleteUser(userId, loginId);

        if (!resultCode.equals(ResponseCode.SUCCESS_CODE)) return ResponseCode.ERROR_CODE;

        session.invalidate();
        return resultCode;

    }

}

마지막으로 Controller 부분은 위처럼 구현하였다.

오늘은 정말 그냥 CRUD만 만드는 것이기 때문에, 특별한 것은 없다. 내가 개발할 수 있는 방법을 동원하여 일단 구현을 하는데에 집중하였다. 리팩토링이나 수정을 하는건 이후의 일이다.

( 이 코드들이 앞으로 어떻게 변하는지도 계속 회고록으로 작성할 것이다. )

여기서 중점은 Session을 계속 체크하는 것과, Validation을 체크하기 위해 validation을 체크하는 메서드를 만들었다는 것이다. ( 추후 @Valid를 사용할 예정이다. )

 

참고 :

회원탈퇴의 경우 외래키가 묶인 게시판들이 남아있으면 회원 탈퇴시 오류가 발생하는데, 그 이유는 DB에서 외래키로 묶여있기 때문이다.  현업에서는 DEL_YN 같은 컬럼을 만들어서, 실제로 회원탈퇴시 바로 DB에서 삭제하지 않고, DEL_YN이 Y인 경우 게시판 글이 안보이게 하는 등으로 처리하는데, 필자는 CRUD 연습이기 때문에 그냥 게시글을 먼저 삭제하고, 회원도 삭제하게끔 설계하고 구현하였다.

 

 


게시판 관련 개발

게시판 기능도 마찬가지로, 작성 수정 삭제 읽기 정도를 개발하였다. 그리고 읽는 도중에는 조회수가 올라갈 수 있게 개발을 진행하였다.

전체적인 코드는 위 회원과 비슷하게 구성되었다.

조금 다른게 있다면 조회수를 올려주는 기능과 게시판 페이징을 처리하는게 있다는건데, 그 부분만 좀 보여주려고 한다.

/**
 * 일반적인 조회수 추가
 */
@Transactional
@Modifying
@Query (value = "UPDATE TB_BOARD SET BOARD_VIEW = BOARD_VIEW + 1 WHERE BOARD_ID = :boardId", nativeQuery = true)
void addBoardView(@Param("boardId") Long boardId);

해당 코드를 통해, 게시글을 조회할 때마다 +1씩 조회수가 업데이트 되게끔 로직을 만들었다.

 

/**
 * 게시판 리스트 페이지로 이동 ( 페이징 처리 10개씩 )
 */
@GetMapping("/boardListPage")
public String boardListPage(HttpSession session, Model model,
                            @RequestParam(value = "startIdx", defaultValue = "0") int startIdx,
                            @RequestParam(value = "page", defaultValue = "1") int page,
                            @RequestParam(value = "pagingType", defaultValue = "0") int pagingType) {

    // Session 끊킬 시 redirect
    User user = (User) session.getAttribute("loginUser");
    boolean loginSession = user != null;
    if ( !loginSession ) return "redirect:/loginPage";
    model.addAttribute("user", user);
    model.addAttribute("loginSession", loginSession);

    // 페이징 처리
    boolean nextPage = false;
    boolean beforePage = false;
    if ( pagingType == 1 ) { // 다음
        startIdx += 10;
        page ++;
    } else if ( pagingType == 2 ) { // 이전
        startIdx -= 10;
        page --;
    }
    if ( page > 1 ) beforePage = true;

    int totalPageCount = boardService.getBoardCount();
    if ( totalPageCount > startIdx + 10 ) nextPage = true;

    int count = 10;
    List<Board> dbBoardList = boardService.getBoardList(startIdx, count);
    List<BoardVo> boardList = new ArrayList<>();
    for ( Board board : dbBoardList ) {
        BoardVo boardVo = new BoardVo();
        boardVo.setBoardId(board.getBoardId());
        boardVo.setTitle(board.getTitle());
        boardVo.setContent(board.getContent());
        boardVo.setBoardView(board.getBoardView() + boardService.getBoardViewRedis(board.getBoardId()));
        boardVo.setCreateDateTime(board.getCreateDateTime());
        boardVo.setUpdateDateTime(board.getUpdateDateTime());
        boardVo.setUserId(board.getUserId());
        boardList.add(boardVo);
    }

    model.addAttribute("boardList", boardList);
    model.addAttribute("startIdx", startIdx);
    model.addAttribute("page", page);
    model.addAttribute("nextPage", nextPage);
    model.addAttribute("beforePage", beforePage);

    return "/boardList";
}

이 부분은 10개씩 게시글을 불러오는 페이징 처리 코드이다. Controller에 작성하였다.

 

게시판 기능은 코드를 전체적으로 적지 않았다.

그 이유는 회원 코드와 유사하고, 코드만 길게 늘어뜨리게 될 뿐이기 때문이다.

무엇보다 처음에는 단순 구현을 목적으로 했기 때문에 코드를 장황하게 적을게 없다.

이제 피드백과, 개인 공부를 통해 해당 코드를 조금씩 바꿔 볼 예정이다.

 

 


해당 CRUD 개발 깃허브 링크 : 

https://github.com/JeonDaehong/JpaNativeQuery-Redis-CRUD-Project

 

GitHub - JeonDaehong/JpaNativeQuery-Redis-CRUD-Project

Contribute to JeonDaehong/JpaNativeQuery-Redis-CRUD-Project development by creating an account on GitHub.

github.com