AA팀 개발자 교육 문서 - Chapter 2
이 장에서는 L/T Framework v3 백엔드(admin-work-api)의 핵심 아키텍처인 CQRS (Command Query Responsibility Segregation) 패턴을 이해하고, Java 코딩 표준, 주석/Swagger 작성 규칙, PageHelper 페이징 처리, 업무 에러 처리(ServiceException), 트랜잭션 및 멀티 DB 설정 가이드를 다룹니다.
L/T Framework의 모든 백엔드 코드에는 Swagger 문서 자동 생성과 가독성을 위해 표준 주석을 명시합니다.
*Controller.java (Swagger 클래스 및 API 스펙 주석):
@Tag(name = "샘플 공지사항 관리", description = "공지사항 조회, 등록, 수정, 삭제 CQRS API")
@RestController
@RequestMapping("/api/v1/sample/notice")
public class SampleNoticeController {
@Operation(summary = "공지사항 목록 조회", description = "조건검색 및 페이징이 적용된 공지사항 목록을 조회합니다.")
@GetMapping("/list")
public TableDataInfo list(SampleNotice notice) { ... }
}
*DTO.java / *Entity.java (Swagger 필드 주석):
@Schema(description = "공지사항 요청/응답 DTO")
@Getter @Setter
public class SampleNoticeDTO extends BaseEntity {
@Schema(description = "공지사항 ID", example = "1001")
private Long noticeId;
@Schema(description = "공지사항 제목", example = "시스템 점검 안내")
private String noticeTitle;
}
*Service.java & *Mapper.java (Javadoc 표준 주석):
/**
* 공지사항 비즈니스 로직 서비스 인터페이스
*/
public interface ISampleNoticeService {
/**
* 공지사항 상세 정보를 조회한다.
* @param noticeId 공지사항 ID
* @return 공지사항 상세 객체
*/
SampleNotice selectNoticeById(Long noticeId);
}
SampleNoticeController, SampleNoticeServiceImpl)selectNoticeList, createNotice)tb_sample_notice, notice_title)모든 REST API URL은 버저닝 규칙을 따릅니다: /api/v1/{domain}/{resource}
CQRS 패턴에 따라 읽기(Query)와 쓰기(Command) 패키지를 명확히 분리합니다.
com.valuesplay.project.db1.sample/
├── controller/
│ └── SampleNoticeController.java # REST Controller
├── service/
│ ├── ISampleNoticeService.java # Service Interface
│ └── impl/
│ └── SampleNoticeServiceImpl.java # Service Implementation (Query & Command Mapper 호출)
├── domain/
│ └── SampleNotice.java # Entity / DTO (BaseEntity 상속)
└── mapper/
├── query/
│ └── SampleNoticeQueryMapper.java # SELECT 전용 Mapper (secondaryQuerySqlSessionFactory)
└── command/
└── SampleNoticeCommandMapper.java # CUD 전용 Mapper (secondaryCommandSqlSessionFactory)
// ServiceImpl
@Override
@Transactional(rollbackFor = Exception.class)
public int insertNotice(SampleNotice notice) {
return sampleNoticeCommandMapper.insertNotice(notice);
}
List<SampleNotice> list = sampleNoticeQueryMapper.selectNoticeListAll(query);
startPage() 호출 직후 실행되는 첫번째 SELECT 쿼리에 페이징 구문을 자동 주입합니다.// Controller
@GetMapping("/list")
public TableDataInfo list(SampleNotice notice) {
startPage(); // PageHelper.startPage() 호출
List<SampleNotice> list = sampleNoticeService.selectNoticeList(notice);
return getDataTable(list); // Total Page 및 TableDataInfo 객체 변환
}
COUNT(*) 쿼리가 둔화되는 경우, MyBatis XML에 수동 Count 쿼리를 세팅하여 성능을 최적화합니다.<!-- SampleNoticeQueryMapper.xml -->
<select id="selectNoticeList_COUNT" resultType="long">
SELECT COUNT(1) FROM tb_sample_notice WHERE del_flag = '0'
</select>
@GetMapping("/{noticeId}")
public AjaxResult getInfo(@PathVariable Long noticeId) {
return success(sampleNoticeService.selectNoticeById(noticeId));
}
@PutMapping
public AjaxResult edit(@RequestBody SampleNotice notice) {
return toAjax(sampleNoticeService.updateNotice(notice));
}
@DeleteMapping("/{noticeIds}")
public AjaxResult remove(@PathVariable Long[] noticeIds) {
return toAjax(sampleNoticeService.deleteNoticeByIds(noticeIds));
}
ServiceException)ErrorCode / ServiceException)비즈니스 예외 발생 시 하드코딩된 문자열 대신 에러 코드와 정의된 예외를 던집니다.
// 예외 던지기 (Service)
if (notice == null) {
throw new ServiceException("존재하지 않거나 이미 삭제된 공지사항입니다.", HttpStatus.NOT_FOUND.value());
}
@Transactional(rollbackFor = Exception.class)를 명시합니다.@Transactional(readOnly = true)를 적용하여 성능을 향상시킵니다.application-{env}.yml)새로운 데이터베이스(dev, prd 등)를 추가할 때는 application-dev.yml에 데이터소스 프로퍼티를 확장하고 secondaryCommandDataSource에 매핑합니다.
# application-dev.yml
spring:
datasource:
dynamic:
datasource:
db1-master:
url: jdbc:postgresql://dev-db.valuesplay.com:5432/admin_db
username: admin_dev
password: ENC(encrypted_password)
db1-slave:
url: jdbc:postgresql://dev-db-read.valuesplay.com:5432/admin_db
username: admin_read
password: ENC(encrypted_password)