🦉 뀨업 - 이화 백준 사이트 리팩토링 ( 4 ) MyBatis Migration

2025. 1. 17. 00:11개발/🦉 뀨업

 

 

 

기존 뀨업 서비스의 코드는 JDBC 기반이었다...

게다가 서비스 인터페이스 정의 안 하고 냅다 서비스 구현체로 코딩했다...

그래서 기능에 대한 수정 회의가 있을 때마다 거의 모든 클래스들 들락날락하면서 코드를 수정했다...

 

이대론 안 되겠다 싶어서

전체적으로 코드 리팩토링을 진행했다!

 

 

🔧 리팩토링 방안과 이유

 

1.  MyBatis로 마이그레이션

SQL 쿼리들이 이리저리 흩어져 있고, 커넥션 관리 때문에 코드가 잔뜩 길어져서 수정하기가 빡세다

->  도메인별 XML 파일에 SQL문 한꺼번에 관리 가능

->  MyBatis의 SQLSession이 커넥션 관리를 대신 해줌

 

2.  Service Interface 작성

기능 구현 방법이 조금씩 계속 수정되고 있기 때문에

필요한 역할들을 Service Interface에 정의해놓고

구현체들을 만들어나가야겠다고 생각했당

 

 


 

 

 

[ MyBatis Migration ]

 

Dependency 추가

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.5.2'

 

SQL문 XML 파일에 작성

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="ggyuel.ggyuup.mainPage.mapper.MainMapper">
    <select id="selectEwhaInfo" resultType="ggyuel.ggyuup.organization.domain.Organizations">
        SELECT ranking, group_name, solved_num
        FROM organizations
        WHERE group_name = #{groupName}
    </select>
    <select id="selectRivalInfo" resultType="ggyuel.ggyuup.organization.domain.Organizations">
        SELECT ranking, group_name, solved_num
        FROM organizations
        WHERE ranking = ((SELECT ranking FROM organizations WHERE group_name = #{groupName}) - 1)
    </select>
    <select id="selectTodayPs" resultType="ggyuel.ggyuup.problem.domain.Problems">
        SELECT p.problem_id, p.title, p.link, p.tier, p.solved_num
        FROM todayps tp
        JOIN problems p ON tp.problem_id = p.problem_id
    </select>
</mapper>

-  MyBatis 관련 설정

-  Mapper Interface 위치를 namespace에 저장

-  id에 Mapper Interface에 정의한 메서드 이름 저장

-  resultType에 반환값 타입 저장

-  SQL문 작성

 

🚨 주의 사항
1.  Mapper Interface 위치 올바르게 명시
     대부분 src > main > java 아래부터 path 작성하면 됨
2.  id값이 Mapper Interface에 정의한 메서드 이름과 동일해야 함
3.  returnType과 SQL문 실행 후 반환값이 일치해야 함

 

 

Mapper Interface 작성

@Mapper
public interface MainMapper {
    Optional<Organizations> selectEwhaInfo(@Param("groupName") String groupName);
    Optional<Organizations> selectRivalInfo(@Param("groupName") String groupName);
    List<Problems> selectTodayPs();
}

-  @Mapper 를 통해 XML 파일과 연결

 

Repository 작성

@Repository
@RequiredArgsConstructor
public class MainRepository {
    private final MainMapper mainMapper;

    public Optional<Organizations> selectEwhaInfo(String groupName) { return mainMapper.selectEwhaInfo(groupName); }
    public Optional<Organizations> selectRivalInfo(String groupName) { return mainMapper.selectRivalInfo(groupName); }
    public List<Problems> selectTodayPs() { return mainMapper.selectTodayPs(); }
}

-  Mapper Interface의 구현체 역할

 

application.yml에 MyBatis 관련 설정

mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 30

-  mapper-locations를 통해서 XML 파일의 위치 정의

 

[ Service Interface ]

 

MemberService Interface

public interface MemberService {
    Boolean checkEwhain(String request);
}

-  checkEwhain : 이화인인지 확인

 

ProblemService Interface

public interface ProblemService {
    List<ProblemAlgoRespDTO> getProblemsByAlgo(String algo);
    List<ProblemTierRespDTO> getProblemsByTier(int tier);
}

-  getProblemsByAlgo : 알고리즘별로 문제 검색

-  getProblemsByTier : 티어별로 문제 검색

 

MainPageService Interface

public interface MainPageService {
    Optional<Organizations> getEwhaInfo();
    Optional<Organizations> getRivalInfo();
    GroupInfoRespDTO getGroupInfo();
    ArrayList<TodayPsRespDTO> getTodayPS();
    MainPageRespDTO getMainPage();
}

-  getEwhaInfo : 이화여대 정보(백준 순위, 푼 문제 수) 조회

-  getRivalInfo : 라이벌 그룹 정보(백준 순위, 푼 문제 수, 그룹 이름) 조회

-  getGroupInfo : 라이벌 그룹과의 푼 문제 수 차이 등등 조회

-  getTodayPS : 오늘의 문제 조회

-  getMainPage : 메인 페이지에 들어갈 정보(라이벌 그룹 정보, 오늘의 문제) 조회

 

 

 


 

 

💣 트러블슈팅

분명 XML 파일에 Mapper 위치도 잘 작성하고

returnType이랑 반환값 매칭도 알맞게 해놨고

application.yml에 XML 파일들 위치도 틀림 없이 작성했는데

아래와 같은 Binding Error가 발생했다...

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): ggyuel.ggyuup.mainPage.mapper.MainMapper.selectEwhaInfo

 

몇번이고 재확인해봐도 도저히 문제가 될 부분이 없어 보였는데...

하... 세상에나...

application.yml 파일의 공백이 문제여따..

 

아래처럼 spring 아래에 mybatis 관련 설정을 해놨다 보니까

mybatis 설정을 아예 인식을 못해서 XML 파일을 못 찾았었나부다...

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ggyuup
    username: root
    password: number5598
    driver-class-name: com.mysql.cj.jdbc.Driver
  sql:
    init:
      mode: never

  mybatis:
    mapper-locations: classpath:mapper/**/*.xml
  	configuration:
      map-underscore-to-camel-case: true
      default-fetch-size: 100
      default-statement-timeout: 30

 

이렇게 수정했더니 해결됐다 휴우..

아아악

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/ggyuup
    username: root
    password: number5598
    driver-class-name: com.mysql.cj.jdbc.Driver
  sql:
    init:
      mode: never

mybatis:
  mapper-locations: classpath:mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    default-statement-timeout: 30

 

 


 

 

+ )  Swagger도 붙였음

 

1 )  Dependency 추가

// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

2 )  Controller class에 설정 추가 - @Tag @Operation

package ggyuel.ggyuup.problem.controller;

import ggyuel.ggyuup.dataCrawling.service.DataCrawlingService;
import ggyuel.ggyuup.problem.dto.ProblemAlgoRespDTO;
import ggyuel.ggyuup.problem.dto.ProblemTierRespDTO;
import ggyuel.ggyuup.problem.service.ProblemService;
import ggyuel.ggyuup.global.apiResponse.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/problems")
@Tag(name = "Problem", description = "Problem API")
public class ProblemController {

    private final ProblemService problemService;
    private final DataCrawlingService dataCrawlingService;


    @GetMapping("/algo")
    @Operation(summary = "알고리즘별 문제 검색", description = "알고리즘별로 문제 정렬")
    public ApiResponse<List<ProblemAlgoRespDTO>> getProblemAlgo(@RequestParam("tag") String algo) {

        List<ProblemAlgoRespDTO> problemAlgoRespDTOList = problemService.getProblemsByAlgo(algo);

        return ApiResponse.onSuccess(problemAlgoRespDTOList);
    }


    @GetMapping("/tier")
    @Operation(summary = "티어별 문제 검색", description = "티어별로 문제 정렬")
    public ApiResponse<List<ProblemTierRespDTO>> getProblemTier(@RequestParam("tier") int tier) {
        List<ProblemTierRespDTO> problemTierRespDTOList = problemService.getProblemsByTier(tier);
        return ApiResponse.onSuccess(problemTierRespDTOList);
    }

    @GetMapping("/refresh")
    @Operation(summary = "문제 리프레시", description = "리프레시 버튼 눌렀을 때 문제 리프레시")
    public void refreshProblems(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for(Cookie cookie : cookies){
                if(cookie.getName().equals("handle")){
                    dataCrawlingService.userRefresh(cookie.getValue());
                }
            }
        }
    }
}