[수업 목표]
Top5 회원을 어떤 기준으로 선정?
API 사용 시간
= Controller 에 요청이 들어온 시간 ~ 응답이 나간 시간
Ex)
수행시간 측정 코드 구현
Intellij 메뉴에서 File > New > Scratch File
수행시간 측정 코드 구현
class Scratch {
public static void main(String[] args) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
// 함수 수행
long output = sumFromOneTo(1_000_000_000);
// 측정 종료 시간
long endTime = System.currentTimeMillis();
long runTime = endTime - startTime;
System.out.println("소요시간: " + runTime);
}
private static long sumFromOneTo(long input) {
long output = 0;
for (int i = 1; i < input; ++i) {
output = output + i;
}
return output;
}
}
컬럼명 | 컬럼타입 | 중복허용 | 설명 |
---|---|---|---|
id | Long | X | 테이블 ID (PK) |
user_id | Long | X | 회원 ID (FK) |
totalTime | Long | O | API 총 사용시간 |
- user_id : User 테이블의 id
(하나의 회원은 하나의 row만 가질 수 있음, 따라서 @OneToOne)
일단, 관심상품 저장하는 API (POST /api/products)에만 적용해보자
model > ApiUseTime
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class ApiUseTime {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@OneToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
@Column(nullable = false)
private Long totalTime;
public ApiUseTime(User user, long totalTime) {
this.user = user;
this.totalTime = totalTime;
}
public void addUseTime(long useTime) {
this.totalTime += useTime;
}
}
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ApiUseTimeRepository extends JpaRepository<ApiUseTime, Long> {
Optional<ApiUseTime> findByUser(User user);
}
- model 하나 만들면 repository 만들기
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class ProductController {
private final ProductService productService;
private final ApiUseTimeRepository apiUseTimeRepository;
@Autowired
public ProductController(
ProductService productService,
ApiUseTimeRepository apiUseTimeRepository
) {
this.productService = productService;
this.apiUseTimeRepository = apiUseTimeRepository;
}
// 신규 상품 등록
@PostMapping("/api/products")
public Product createProduct(@RequestBody ProductRequestDto requestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 로그인 되어 있는 회원 테이블의 ID
Long userId = userDetails.getUser().getId();
Product product = productService.createProduct(requestDto, userId);
// 응답 보내기
return product;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원 정보
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면
apiUseTime.addUseTime(runTime);
}
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
...
}
- try-catch 문 사용, (finally는 예외 있던 없던 항상 수행)
- 시간 누적을 어떻게 할까?
: 이미 이 User에 대해서 ApiUseTime에 존재하면 그냥 더해주기
(Optional -> .orElse(null) -> Optional에 대한 처리(nullPointerException 방지)
Name | Method | URL | 설명 |
---|---|---|---|
API 총 사용시간 조회 | GET | /api/use/time | 회원별 API 총 사용시간 조회 (관리자용) |
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class ApiUseTimeController {
private ApiUseTimeRepository apiUseTimeRepository;
@Autowired
public ApiUseTimeController(ApiUseTimeRepository apiUseTimeRepository) {
this.apiUseTimeRepository = apiUseTimeRepository;
}
@Secured(UserRoleEnum.Authority.ADMIN)
@GetMapping("/api/use/time")
public List<ApiUseTime> getAllApiUseTime() {
return apiUseTimeRepository.findAll();
}
}
...
@Override
public void run(ApplicationArguments args) throws Exception {
// 테스트 User 생성
User testUser1 = new User("정국", passwordEncoder.encode("123"), "jg@sparta.com", UserRoleEnum.USER);
User testUser2 = new User("제이홉", passwordEncoder.encode("123"), "hope@sparta.com", UserRoleEnum.USER);
User testAdminUser1 = new User("아미", passwordEncoder.encode("123"), "army@sparta.com", UserRoleEnum.ADMIN);
testUser1 = userRepository.save(testUser1);
testUser2 = userRepository.save(testUser2);
testAdminUser1 = userRepository.save(testAdminUser1);
// 테스트 User 의 관심상품 등록
// 검색어 당 관심상품 10개 등록
createTestData(testUser1, "신발");
createTestData(testUser1, "과자");
createTestData(testUser1, "키보드");
createTestData(testUser1, "휴지");
createTestData(testUser1, "휴대폰");
createTestData(testUser1, "앨범");
createTestData(testUser1, "헤드폰");
createTestData(testUser1, "이어폰");
createTestData(testUser1, "노트북");
createTestData(testUser1, "무선 이어폰");
createTestData(testUser1, "모니터");
}
...
관심상품 등록시마다 API 사용시간이 잘 저장되는지 확인
회원별 총 사용시간 조회 API 동작 검증
'Top5 회원 찾기' 기능이 추가된 것을 '나만의 셀렉샵' 회원들이 알 수 있을까? 혹은 알 필요가 있을까?
- 02에서는 부가기능 사이에 핵심기능을 넣어서 했었음
모든 '핵심기능'의 Controller에 '부가기능' 코드를 추가했을 때..
'핵심기능'이 100면?
'핵심기능'이 나중에 추가되면?
'핵심기능' 수정 시
'부가기능' 수정 시
'부가기능' 삭제
- 따라서 AOP 등장!!
: 스프링이 부가기능과 핵심기능을 붙여주는 것처럼 해준다
1. 어드바이스 : 부가기능
2. 포인트컷 : 부가기능 적용 위치
- 부가기능 적용위치에만 넣어달라!
- ex) Controller에만 넣어줘, Service & Repository 말고
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
this.apiUseTimeRepository = apiUseTimeRepository;
}
// advice
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
// 측정 시작 시간
long startTime = System.currentTimeMillis();
try {
// 핵심기능 수행
Object output = joinPoint.proceed(); // 결과가 ouput으로 들어옴
return output;
} finally {
// 측정 종료 시간
long endTime = System.currentTimeMillis();
// 수행시간 = 종료 시간 - 시작 시간
long runTime = endTime - startTime;
// 로그인 회원이 없는 경우(로그인 안하면), 수행시간 기록하지 않음
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
// 로그인 회원 정보
UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
User loginUser = userDetails.getUser();
// API 사용시간 및 DB 에 기록
ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser)
.orElse(null);
if (apiUseTime == null) {
// 로그인 회원의 기록이 없으면
apiUseTime = new ApiUseTime(loginUser, runTime);
} else {
// 로그인 회원의 기록이 이미 있으면
apiUseTime.addUseTime(runTime);
}
System.out.println("[API Use Time] Username: " + loginUser.getUsername() + ", Total Time: " + apiUseTime.getTotalTime() + " ms");
apiUseTimeRepository.save(apiUseTime);
}
}
}
}
- 핵심 기능1 바로 호출하지 않고 프록시라는 객체가 생성되어
이것이 핵심기능1을 호출 (프록시가 부가기능 역할)
포인트컷
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
@Around("execution(public * com.sparta.springcore.controller..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
modifiers-pattern
return-type-pattern
declaring-type-pattern
method-name-pattern(param-pattern)
@Pointcut
@Component // 빈 등록해야 @Aspect 적용 가능 (AOP로 사용 가능)
@Aspect
public class Aspect {
@Pointcut("execution(* com.sparta.springcore.controller.*.*(..))")
private void forAllController() {}
// View를 return 하는 것은 String이라고 가정(위에랑 String 만 다름)
@Pointcut("execution(String com.sparta.springcore.controller.*.*())")
private void forAllViewController() {}
@Around("forAllContorller() && !forAllViewController")
public void saveRestApiLog() {
...
}
@Around("forAllContorller()")
public void saveAllApiLog() {
...
}
}
폴더들을 저장 시, "중복 폴더명 저장 시" 에러를 발생시킴
요구사항에서 사용되는 용어 중 AS-IS, TO-BE 가 있음.
AS-IS: 기존 동작
TO-BE: 변경 동작
AS-IS)
TO-BE)
Folder folder = new Folder(searchWord, user);
folderRepository.save(folder);
// 로그인한 회원에 폴더들 등록
public List<Folder> addFolders(List<String> folderNames, User user) {
// 1) 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
List<Folder> savedFolderList = new ArrayList<>();
for (String folderName : folderNames) {
// 2) 이미 생성한 폴더가 아닌 경우만 폴더 생성
if (isExistFolderName(folderName, existFolderList)) {
// Exception 발생!
throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
} else {
Folder folder = new Folder(folderName, user);
// 폴더명 저장
folder = folderRepository.save(folder);
savedFolderList.add(folder);
}
}
return savedFolderList;
}
문제점
- ex) 중복되지 않는 폴더 2개와 중복된 폴더명을 추가하면?
3개 다 저장 안되어야 하는데
중복되지 않는 폴더 2개가 DB에 저장됨
folderRepository.deleteAll(savedFolderList);
트랜잭션 : 데이터베이스에서 데이터에 대한 하나의 논리적 실행단계
(DB 입장에서 더이상 쪼갤 수 없는 최소 단위의 작업)
ACID (원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어
출처 : 위키백과
User user = new User(username, password, email, role);
user = userRepository.save(user);
문제점 : PASSWORD 저장 안됨, ROLE이 ADMIN으로 저장
-> DB가 아예 저장 안되도록 함. 이것을 지켜주기 위한 것이 ACID다!!
A 계좌 → B계좌로 200,000 원 이체 시
정상 케이스
만약, 3번 과정에서 에러가 발생 시
-> 이를 해결하기 위해 트랜잭션이라는 개념 추가
트랜잭션 시작
모두 성공 시 ⇒트랜잭션 Commit
중간에 하나라도 실패 시 ⇒ 트랜잭션 Rollback
(트랜잭션 시작 전으로 돌아감)
public List<Folder> addFolders(List<String> folderNames, User user) {
// 트랜잭션의 시작
// transactionManager 만들어서 이를 가지고 Transaction 가저와서 트랜잭션 시작됐다라는 것을 DB에게 알려줌
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 1) 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);
List<Folder> savedFolderList = new ArrayList<>();
for (String folderName : folderNames) {
// 2) 이미 생성한 폴더가 아닌 경우만 폴더 생성
if (isExistFolderName(folderName, existFolderList)) {
// Exception 발생!
throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
} else {
Folder folder = new Folder(folderName, user);
// 폴더명 저장
folder = folderRepository.save(folder);
savedFolderList.add(folder);
}
}
// 트랜잭션 commit (정상)
transactionManager.commit(status);
return savedFolderList;
} catch (Exception ex) {
// 트랜잭션 rollback (Exception 발생)
transactionManager.rollback(status);
throw ex;
}
}
- 트랜잭션 시작을 DB에게 알려줌.
- DB가 트랜잭션 아이디를 발급해줌
- 그 아이디로 commit 또는 rollback 해준다.
@Transactional
public List<Folder> addFolders(List<String> folderNames, User user) {
// ...
}
-> @Transactional 하면 AOP(핵심 기능, 부가 기능 나눈 것과 비슷)가 동작하는 것.
(AOP에 의해서 Transaction 부가기능이 나타남 -> 프록시가 생긴 것)
다음과 같이 코드를 리팩토링하면 문제가 생김.
어떤 문제일까?
리팩토링 코드
repository > FolderRepository
public interface FolderRepository extends JpaRepository<Folder, Long> {
List<Folder> findAllByUser(User user);
// User 명과 Folder 이름을 가지고 존재하면 true
boolean existsByUserAndName(User user, String name);
}
service > FolderService
// 로그인한 회원에 폴더들 등록
public List<Folder> addFolders(List<String> folderNames, User user) {
List<Folder> savedFolderList = new ArrayList<>();
for (String folderName : folderNames) {
Folder folder = createFolderOrThrow(folderName, user);
savedFolderList.add(folder);
}
return savedFolderList;
}
@Transactional
public Folder createFolderOrThrow(String folderName, User user) {
// 입력으로 들어온 폴더 이름이 이미 존재하는 경우, Exception 발생
boolean isExistFolder = folderRepository.existsByUserAndName(user, folderName);
if (isExistFolder) {
throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
}
// 폴더명 저장
Folder folder = new Folder(folderName, user);
return folderRepository.save(folder);
}
-> DB에 이미 저장된 폴더명들이 Rollback 되지 않음
- 이유 : 하나의 folder 생성마다 하나의 트랜잭션으로 처리되어서
// 로그인한 회원에 폴더들 등록
@Transactional
public List<Folder> addFolders(List<String> folderNames, User user) {
List<Folder> savedFolderList = new ArrayList<>();
for (String folderName : folderNames) {
Folder folder = createFolderOrThrow(folderName, user);
savedFolderList.add(folder);
}
return savedFolderList;
}
public Folder createFolderOrThrow(String folderName, User user) {
// 입력으로 들어온 폴더 이름이 이미 존재하는 경우, Exception 발생
boolean isExistFolder = folderRepository.existsByUserAndName(user, folderName);
if (isExistFolder) {
throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
}
// 폴더명 저장
Folder folder = new Folder(folderName, user);
return folderRepository.save(folder);
}
Ex)
- 회원 A 계좌 잔고: 100만원
- DB1: 100만원
- DB2: 100만원
1. 70만원 인출 시도
1. DB1 에서 70만원 인출하여 잔고 30만원
2. DB2 에서도 동일하게 잔고 30만원으로 데이터 Sync 필요!
2. 50만원 인출 시도
1. DB1 를 통해 잔고 확인 시
- 회원 A 의 "잔고 30만원"이기 때문에, 인출불가 에러 발생
2. DB2 를 통해 잔고 확인 시
- DB2 에 "첫번째 인출 시도한 데이터(70만원)"가 Sync 적용되기 전이라고 가정
- 혹은 DB2 에 데이터 Sync 중 에러가 발생했다고 가정
- DB2 에서는 회원 A의 "잔고 100만원" 이 남아 있다고 판단
- 50만원이 정상 인출됨
3. DB1 과 DB2 의 데이터 불일치 ⇒ 어느 정보를 믿어야하지?
1. DB1 에서 회원 A 의 잔고: 30만원
2. DB2 에서 회원 B 의 잔고: 50만원
@Transactional(readOnly = false)
@Transactional(readOnly = true)
수십년 동안 통용되던 용어 대체됨
마스터 (Master) -> Primary
슬레이브 (Slave) -> Replica, Secondary
출처 : 위키백과
HTTP 에러 메시지 전달 방법 이해
참고 : HTTP 메시지 설명 (MDN Web Docs)
Response 메시지
HTTP/1.1 404 Not Found
HTTP 상태 코드 종류
<1> 2xx Success
<2> 4xx Client Error
<3> 5xx Server Error
public enum HttpStatus {
// 1xx Informational
CONTINUE(100, Series.INFORMATIONAL, "Continue"),
// ...
// 2xx Success
OK(200, Series.SUCCESSFUL, "OK"),
CREATED(201, Series.SUCCESSFUL, "Created"),
// ...
// 3xx Redirection
MULTIPLE_CHOICES(300, Series.REDIRECTION, "Multiple Choices"),
MOVED_PERMANENTLY(301, Series.REDIRECTION, "Moved Permanently"),
FOUND(302, Series.REDIRECTION, "Found"),
// ...
// --- 4xx Client Error ---
BAD_REQUEST(400, Series.CLIENT_ERROR, "Bad Request"), // 잘못 요청
UNAUTHORIZED(401, Series.CLIENT_ERROR, "Unauthorized"), // 인가되지 않은거 요청
PAYMENT_REQUIRED(402, Series.CLIENT_ERROR, "Payment Required"),
FORBIDDEN(403, Series.CLIENT_ERROR, "Forbidden"), // 숨겨져있는 거 요청
// ...
// --- 5xx Server Error ---
INTERNAL_SERVER_ERROR(500, Series.SERVER_ERROR, "Internal Server Error"), // 예상치 못한 에러
NOT_IMPLEMENTED(501, Series.SERVER_ERROR, "Not Implemented"),
BAD_GATEWAY(502, Series.SERVER_ERROR, "Bad Gateway"),
// ...
}
현재 에러 발생 시 HTTP 에러 메시지 확인
: 어떻게 throw로 클라이언트에게 Http 코드로 어떻게 내려가는지 확인
throw new IllegalArgumentException("중복된 폴더명을 제거해 주세요! 폴더명: " + folderName);
HTTP 500 Internal Server Error
참고: MDN Web Docs
{
"errorMessage":"중복된 폴더명을 제거해 주세요! 폴더명: 신발",
"httpStatus":"BAD_REQUEST"
}
Controller 코드 수정
a. 프론트엔드 : 에러메시지 팝업 띄우는 작업 적용
: ajax에서 POST /api/folders 요청 후,
성공했을 때 처리 말고 error에 대한 처리가 들어옴
b. 백엔드 : 예외 처리
: 본문에 담아서 보내줄 JSON 내리는 형태
import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;
@Getter
@Setter
public class RestApiException {
private String errorMessage;
private HttpStatus httpStatus;
}
: Controller에서 에러 처리 안해주면 스프링으로 올라옴
(이를 스프링이 500에러로 내려준다)
@PostMapping("api/folders")
public ResponseEntity addFolders(
@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
try {
List<String> folderNames = folderRequestDto.getFolderNames();
User user = userDetails.getUser();
List<Folder> folders = folderService.addFolders(folderNames, user);
return new ResponseEntity(folders, HttpStatus.OK); // 정상 케이스
} catch(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException();
restApiException.setHttpStatus(HttpStatus.BAD_REQUEST); // 400 에러
restApiException.setErrorMessage(ex.getMessage()); // 에러 메시지 넘겨줌
return new ResponseEntity(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.BAD_REQUEST);
}
}
@PostMapping("api/folders")
public List<Folder> addFolders(
@RequestBody FolderRequestDto folderRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
List<String> folderNames = folderRequestDto.getFolderNames();
User user = userDetails.getUser();
return folderService.addFolders(folderNames, user);
}
@ExceptionHandler({ IllegalArgumentException.class }) // 어떤 종류 에러 받을 것인지 (여러 개 가능)
public ResponseEntity handleException(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException();
restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
restApiException.setErrorMessage(ex.getMessage());
return new ResponseEntity(
// HTTP body
restApiException,
// HTTP status code
HttpStatus.BAD_REQUEST
);
}
HTTP 400 Bad Request
참고 : MDN Web Docs
public Product updateProduct(Long id, ProductMypriceRequestDto requestDto) {
int myprice = requestDto.getMyprice();
if (myprice < MIN_MY_PRICE) {
throw new IllegalArgumentException("유효하지 않은 관심 가격입니다. 최소 " + MIN_MY_PRICE + " 원 이상으로 설정해 주세요.");
}
}
: Client랑 Controller 사이에서 ControllerAdvice가
Controller인 척하면서 try-catch 다 처리해줌,
(에러 발생해서 throw 되면 Global 하게 Exceptionn 처리해줌)
ExceptionHandler 여러 개 처리 O
: JSON 형태로 Global로 적용됨
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class RestApiExceptionHandler {
@ExceptionHandler(value = { IllegalArgumentException.class })
public ResponseEntity<Object> handleApiRequestException(IllegalArgumentException ex) {
RestApiException restApiException = new RestApiException();
restApiException.setHttpStatus(HttpStatus.BAD_REQUEST);
restApiException.setErrorMessage(ex.getMessage());
return new ResponseEntity(
restApiException,
HttpStatus.BAD_REQUEST
);
}
}
import org.springframework.http.HttpStatus;
import static com.sparta.springcore.service.ProductService.MIN_MY_PRICE;
public enum ErrorCode {
// 400 Bad Request
DUPLICATED_FOLDER_NAME(HttpStatus.BAD_REQUEST, "400_1", "중복폴더명이 이미 존재합니다."),
BELOW_MIN_MY_PRICE(HttpStatus.BAD_REQUEST, "400_2", "최저 희망가는 최소 " + MIN_MY_PRICE + " 원 이상으로 설정해 주세요."),
// 404 Not Found
NOT_FOUND_PRODUCT(HttpStatus.NOT_FOUND, "404_1", "해당 관심상품 아이디가 존재하지 않습니다."),
NOT_FOUND_FOLDER(HttpStatus.NOT_FOUND, "404_2", "해당 폴더 아이디가 존재하지 않습니다."),
;
private final HttpStatus httpStatus;
private final String errorCode;
private final String errorMessage;
ErrorCode(HttpStatus httpStatus, String errorCode, String errorMessage) {
this.httpStatus = httpStatus;
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}
<1> httpStatus : HTTP 상태코드
<2> errorCode : 에러코드
<3> errorMessage
AS-IS
컬럼명 | 컬럼타입 | 중복허용 | 설명 |
---|---|---|---|
id | Long | X | 테이블 ID (PK) |
user_id | Long | X | 회원 ID (FK) |
totalTime | Long | O | API 총 사용시간 |
TO-BE
컬럼명 | 컬럼타입 | 중복허용 | 설명 |
---|---|---|---|
id | Long | X | 테이블 ID (PK) |
user_id | Long | X | 회원 ID (FK) |
totalTime | Long | O | API 총 사용시간 |
totalCount | Long | O | API 총 호출횟수 |
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class ApiUseTime {
// ID가 자동으로 생성 및 증가합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
private Long id;
@OneToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
@Column(nullable = false)
private Long totalTime;
@Column(nullable = false, columnDefinition = "bigint default 0")
private Long totalCount;
public ApiUseTime(User user, long totalTime) {
this.user = user;
this.totalTime = totalTime;
this.totalCount = 1L;
}
public void addUseTime(long useTime) {
this.totalTime += useTime;
this.totalCount += 1L;
}
}
5주차에서는 AOP라는 새로운 개념을 배웠다. 데이터베이스 수업에서 배우던 트랜잭션 개념도 나와 재미있게 들었다. 모든 강의를 다 듣고 정리까지 마쳤으니 이제 복습하면서 프로젝트에 써먹을 날이 왔으면 좋겠다. '나만의 셀렉샵' 코드는 내 깃허브에 올려두었다. 앞으로 차근차근 계속 공부해야지!! 아즈아😬
출처: 스파르타 코딩 클럽