Post
신규 웹 프로젝트 - 2 (백엔드 개발)
1.백엔드 기본 플로우
프론트엔드에서 api호출
Controller -> Service(Interface) -> ServiceImpl (로직 구현) -> Mapper(Mybatis 매퍼 Interface) -> Mapper.xml(쿼리 호출)
로 구성됩니다.
- Controller
- 설명: 클라이언트(프론트엔드)로부터 HTTP 요청을 받아 처리하는 진입점 역할을 합니다. 주로 요청 데이터를 받아서 Service 계층으로 전달하고, Service에서 처리된 결과를 클라이언트에 응답으로 반환합니다. Spring 프레임워크에서는 @RestController 어노테이션을 사용하여 RESTful API를 구현합니다.
- 예시 역할: URL 경로 매핑, 요청 파라미터 검증, 응답 형식(JSON 등) 정의.
- Service (Interface)
- 설명: 비즈니스 로직을 정의하는 인터페이스로, 실제 구현체(ServiceImpl)와의 계약 역할을 합니다. Controller에서 호출되며, 데이터 처리 흐름을 관리합니다.
- 예시 역할: 여러 Mapper 호출을 조합하거나 트랜잭션 관리 등 비즈니스 로직의 설계.
- ServiceImpl
- 설명: Service 인터페이스의 구현체로, 실제 비즈니스 로직을 작성합니다. 데이터 가공, 조건 처리, 예외 처리 등 구체적인 로직이 이곳에 포함됩니다. MyBatis의 Mapper를 호출해 DB 작업을 수행합니다.
- 예시 역할: 입력 데이터를 기반으로 Mapper를 호출해 쿼리 실행 후 결과를 가공.
- Mapper (Interface)
- 설명: MyBatis에서 사용하는 데이터베이스 쿼리 인터페이스로, SQL 쿼리와 자바 객체를 매핑합니다. Mapper.xml 파일과 연결되어 있으며, 메서드 이름과 XML의 쿼리 ID가 일치합니다.
- 예시 역할: selectUserById, insertUser 등 DB 작업 메서드 정의.
- Mapper.xml
- 설명: MyBatis의 SQL 쿼리를 작성하는 XML 파일로, 실제 데이터베이스 쿼리와 결과를 객체에 매핑하는 역할을 합니다. 동적 쿼리 작성(<if>, <foreach> 등)이 가능해 복잡한 쿼리도 처리할 수 있습니다.
- 예시 역할: SELECT, INSERT, UPDATE 등의 SQL 쿼리 작성 및 파라미터 매핑.
Service Interface (바로 호출해도 되지만..!?)
- 추상화와 유연성: 인터페이스를 사용하면 구현체(ServiceImpl)를 언제든지 교체할 수 있습니다. 예를 들어, Mock 객체로 대체해 테스트하거나, 다른 로직으로 쉽게 전환할 수 있습니다.
- 의존성 주입(DI): Spring의 IoC 컨테이너에서 인터페이스를 기반으로 의존성을 주입받아 결합도를 낮춥니다.
- 유지보수성: 코드 변경 시 인터페이스만 유지하면 구현체를 수정해도 상위 계층(Controller)에 영향을 주지 않습니다.
- 설계 관점: 비즈니스 로직의 계약을 명확히 정의해 팀 간 협업이나 코드 가독성을 높입니다.
JPA 포기 이유
복잡한 쿼리가 너무 많고 Query DSL등 러닝커브가 너무 크고, 빠른 개발속도가 필요하기 때문에 적합하지 않음
Controller 로직
@RestController @RequestMapping(“/api/menu”) 컨트롤러 최상단
@GetMapping(“/list”) public ResponseEntity<CustomPage<Menu>> getMenu( @RequestParam(required = false) String menuNm, @RequestParam(defaultValue = “0”) int page, @RequestParam(defaultValue = “30”) int size) { try { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (!(principal instanceof CustomUserDetails)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); } CustomUserDetails userDetails = (CustomUserDetails) principal; String ssnEnterCd = userDetails.getEnterCd(); CustomPage<Menu> menus = menuService.getMenuList(ssnEnterCd, menuNm, page, size); return ResponseEntity.ok(menus); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new CustomPage<>(new ArrayList<>(), 0, size, page)); } }
GetMapping /list
페이징 처리 위해 page와 size를 받음, 조회 조건을 위해 menuNm을 받음
principal쪽은 기존 인증 쪽 로직에서 enterCd와 sabun을 받기 위해 추가
model 객체로 컬럼들 정의
@PostMapping(“/insert”) public ResponseEntity<Menu> insertMenu(@RequestBody Menu menu) { try { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (!(principal instanceof CustomUserDetails)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); } CustomUserDetails userDetails = (CustomUserDetails) principal; //로그인한 사용자 사번 menu.setChkid(userDetails.getUsername());//chkid 세팅 menu.setEnterCd(userDetails.getEnterCd());//enterCd 세팅 Menu insertMenu = menuService.insertMenu(menu); return ResponseEntity.status(201).body(insertMenu); // 201 Created } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(null); // 잘못된 요청 데이터 } catch (Exception e) { return ResponseEntity.status(500).body(null); // 서버 오류 } }
PostMapping /insert
이력관리 위한 인증쪽 sabun이 chkid, enterCd를 받아서 처리
@PutMapping(“/update”) public ResponseEntity<Menu> updateMenu(@RequestBody Menu menu) { try { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (!(principal instanceof CustomUserDetails)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); } CustomUserDetails userDetails = (CustomUserDetails) principal; //로그인한 사용자 사번 menu.setChkid(userDetails.getUsername());//chkid 세팅 menu.setEnterCd(userDetails.getEnterCd());//enterCd 세팅 Menu updateMenu = menuService.updateMenu(menu); return ResponseEntity.ok(updateMenu); // 200 OK } catch (IllegalArgumentException e) { return ResponseEntity.status(400).body(null); // 잘못된 요청 데이터 } catch (RuntimeException e) { return ResponseEntity.status(404).body(null); // 사용자를 찾을 수 없는 경우 } catch (Exception e) { return ResponseEntity.status(500).body(null); // 서버 오류 } }
PutMapping /update
이력관리 위한 인증쪽 sabun이 chkid, enterCd를 받아서 처리
@DeleteMapping(“/delete”) public ResponseEntity<Map<String, Object>> deleteMenu(@RequestBody List<Map<String, String>> menus) { try { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (!(principal instanceof CustomUserDetails)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); } CustomUserDetails userDetails = (CustomUserDetails) principal; //로그인한 사용자 사번 String ssnEnterCd = userDetails.getEnterCd(); List<String> menuList = menus.stream() .map(menu -> menu.get(“menuId”)) .collect(Collectors.toList()); Map<String, Integer> result = menuService.deleteMenu(ssnEnterCd, menuList); Map<String, Object> response = new HashMap<>(); response.put(“succeeded”, result.get(“succeeded”)); response.put(“failed”, result.get(“failed”)); return ResponseEntity.ok(response); } catch (Exception e) { Map<String, Object> errorResponse = new HashMap<>(); errorResponse.put(“succeeded”, 0); errorResponse.put(“failed”, 0); errorResponse.put(“error”, e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } }
DeleteMapping /delete
여러 행 삭제와 단일 삭제를 통합하기 위해 List로 받음
MenuSerive 인터페이스 public interface MenuService { List<Menu> getMenuTree(String enterCd); Menu insertMenu(Menu menu); Menu updateMenu(Menu menu); Map<String, Integer> deleteMenu(String enterCd, List<String> menuId); CustomPage<Menu> getMenuList(String enterCd, String keyword, int page, int size); }
MenuServiceImpl 로직구현 @Override public CustomPage<Menu> getMenuList(String enterCd, String keyword, int page, int size) { Map<String, Object> params = PagingUtils.getPagingParams(page, size); params.put(“enterCd”, enterCd); params.put(“keyword”, keyword);
List<Menu> menus = menuMapper.getMenuList(params); long total = menuMapper.menuCountAll(params);
return new CustomPage<>(menus, total, size, page); }
@Override public Menu insertMenu(Menu menu) { menuMapper.insertMenu(menu); return menu; }
@Override public Menu updateMenu(Menu menu) { menuMapper.updateMenu(menu); return menu; }
@Transactional @Override public Map<String, Integer> deleteMenu(String enterCd, List<String> menuId) { try { Map<String, Object> paramMap = new HashMap<>(); paramMap.put(“enterCd”, enterCd); paramMap.put(“menuId”, menuId); menuMapper.deleteMenu(paramMap);
Map<String, Integer> resultMap = new HashMap<>(); resultMap.put(“succeeded”, menuId.size()); resultMap.put(“failed”, 0); return resultMap; } catch (Exception e) { Map<String, Integer> errorMap = new HashMap<>(); errorMap.put(“succeeded”, 0); errorMap.put(“failed”, menuId.size()); return errorMap; } }
MenuMapper 인터페이스 xml 호출 @Repository public interface MenuMapper { List<Menu> selectMenuHierarchy(String enterCd); // 계층 메뉴 조회 void insertMenu(Menu menu); // 메뉴 추가 void updateMenu(Menu menu); // 메뉴 수정 void deleteMenu(Map<String, Object> params); // 메뉴 삭제 List<Menu> getMenuList(Map<String, Object> params); // 검색 조건 포함 long menuCountAll(Map<String, Object> params); // 페이징 처리 위한 카운트 }
xml <!-- 페이징된 메뉴 목록 조회 (키워드 필터링 포함) --> <select id=“getMenuList” resultMap=“menuResultMap” parameterType=“map”> SELECT MENU_ID, PARENT_MENU_ID, MENU_LABEL, MENU_PATH, MENU_ICON, SEQ, USE_YN, CHKID, CHKDATE FROM ( SELECT A.*, ROWNUM RN FROM ( SELECT M.* FROM TSYS301_NEW M WHERE 1 = 1 AND M.ENTER_CD = #{enterCd} <if test=“keyword != null and keyword != ‘’”> AND (UPPER(M.MENU_LABEL) LIKE UPPER(CAST(#{keyword} AS VARCHAR2(100))) || ‘%’ OR UPPER(M.MENU_PATH) LIKE UPPER(CAST(#{keyword} AS VARCHAR2(100))) || ‘%’)
</if> ORDER BY M.SEQ ) A ) WHERE RN BETWEEN #{startRow} + 1 AND #{endRow} </select>
<!-- 페이징된 전체 메뉴 목록 조회 --> <select id=“menuCountAll” resultType=“long”> SELECT COUNT(*) FROM TSYS301_NEW M WHERE 1 = 1 AND ENTER_CD = #{enterCd} <if test=“keyword != null and keyword != ‘’”> AND (UPPER(M.MENU_LABEL) LIKE UPPER(CAST(#{keyword} AS VARCHAR2(100))) || ‘%’ OR UPPER(M.MENU_PATH) LIKE UPPER(CAST(#{keyword} AS VARCHAR2(100))) || ‘%’) </if> </select>
<insert id=“insertMenu” parameterType=“org.kms.ssms.model.Menu”> INSERT INTO TSYS301_NEW ( ENTER_CD, MENU_ID, PARENT_MENU_ID, MENU_LABEL, MENU_PATH, MENU_ICON , SEQ, USE_YN, CHKID, CHKDATE ) VALUES ( #{enterCd}, #{menuId}, #{parentMenuId}, #{menuLabel}, #{menuPath}, #{menuIcon} , #{seq}, #{useYn}, #{chkid}, SYSDATE ) </insert>
<update id=“updateMenu” parameterType=“org.kms.ssms.model.Menu”> UPDATE TSYS301_NEW SET PARENT_MENU_ID = #{parentMenuId}, MENU_LABEL = #{menuLabel}, MENU_PATH = #{menuPath}, MENU_ICON = #{menuIcon}, SEQ = #{seq}, USE_YN = #{useYn}, CHKID = #{chkid}, CHKDATE = SYSDATE WHERE ENTER_CD = #{enterCd} AND MENU_ID = #{menuId} </update>
<delete id=“deleteMenu” parameterType=“map”> DELETE FROM TSYS301_NEW WHERE ENTER_CD = #{enterCd} AND MENU_ID IN <foreach collection=“menuId” item=“menuId” open=”(“ separator=”,” close=”)”> #{menuId} </foreach> </delete>
여기까지가 기본 로직 구현이고,
추가적인 부분은 따로 추가하면 됩니다.
기본 조회, 입력, 수정, 삭제 는 해당 백엔드 로직을 참고해서 추가하면 됩니다.
백엔드쪽 먼저 맞추고 익숙하지 않은 vue는 GPT 추천.
vue3, primevue 버전(ex.4.3.2) 명시해줘야 대답이 더 괜찮음
댓글