ν¨ν€μ§λͺ , ν΄λμ€λͺ μ μΌλ°μ μΈ κ΄λ‘λλ‘ μμ±νμ΄μ.
μΌλ°μ μΈ λ³μλ camelCase λ°©μμΌλ‘ μμ±νκ³ , Enumμ΄λ μ μ μμλ SNAKE_CASE λ°©μμΌλ‘ μμ±νμ΄μ.
API ꡬνμ μ΅λν RESTfull νκ² νλ €κ³ λ Έλ ₯νμ΄μ.
OS : AWS EC2 - Amazon Linux 2
DB : MariaDB 10.5.16 for Linux
Language : Java - openjdk 11.0.13 2021-10-19 LTS
Frameworks : Spring core 5.3.16, Spring Boot 2.6.4
κΉνλΈλ₯Ό ν΅ν΄ API λ¬Έμλ₯Ό μ 곡νμ΄μ.
Notion, Discord λ₯Ό ν΅ν΄ λΉλλ©΄μΌλ‘ νμ μ μ§ννμ΄μ.
Spring Framework : κ°μ²΄μ§ν₯ νλ‘κ·Έλλ°μΌλ‘ μ£Όμ΄μ§ λΉμ¦λμ€ λ‘μ§ κ΅¬νμ μ§μ€ν μ μλλ‘ Spring Frameworkλ₯Ό μ¬μ©νμ΄μ.
Spring Boot : Spring Frameworkμ λ¨μ μΈ λ³΅μ‘ν μμ‘΄μ± μ€μ μ μλμΌλ‘ ν΄μ€μΌλ‘μ¨ μμ°μ±μ λμ¬μ£Όλ Spring Bootλ₯Ό μ¬μ©νμ΄μ.
- νΈλμ μ
μ€μ μ μ© μλ¨
- λͺλͺ κΈ°λ₯μ κ²½μ° λ³΄λ€ μμ κΈ°λ₯μ μ‘°ν©μΌλ‘ μ΄λ£¨μ΄μ§λλ°, κ°κ°μ μμ κΈ°λ₯μμ μ΄λ€ μ€λ₯κ° λ°μν κ²½μ° λͺ¨λ μμ κΈ°λ₯λ€μ΄ μ€νλκΈ° μ΄μ μΌλ‘ μνλ₯Ό λλλ €μΌ ν©λλ€. μ΄λ₯Ό μν΄ μ€νλ§μμ @
Transactional
μ΄λ Έν μ΄μ μ μ 곡ν©λλ€. κ·Έλ¬λ, μ΄ μ€μ μ΄ μ λλ‘ μ μ©λμ§ μλ νμμ΄ μμμ΅λλ€. - ν΄λΉ μ΄λ Έν μ΄μ μ΄ μ μ©λ λ©μλ λ΄λΆμμ try-catchλ¬Έμ΄ λμνλ κ²½μ° νΈλμ μ μ΄λ Έν μ΄μ μ΄ μ λλ‘ λμνμ§ μλ κ²½μ°κ° μλ€λ μ 보λ₯Ό μ°Ύμκ³ , μ΄λ₯Ό μ μ©ν΄ ν΄κ²°νμ΅λλ€.
- λͺλͺ κΈ°λ₯μ κ²½μ° λ³΄λ€ μμ κΈ°λ₯μ μ‘°ν©μΌλ‘ μ΄λ£¨μ΄μ§λλ°, κ°κ°μ μμ κΈ°λ₯μμ μ΄λ€ μ€λ₯κ° λ°μν κ²½μ° λͺ¨λ μμ κΈ°λ₯λ€μ΄ μ€νλκΈ° μ΄μ μΌλ‘ μνλ₯Ό λλλ €μΌ ν©λλ€. μ΄λ₯Ό μν΄ μ€νλ§μμ @
- CORS μμΈμ²λ¦¬ μ€μ μ μ© μλ¨
- 보μμ μ΄μ λ‘ λΈλΌμ°μ μμλ μ£Ό μμ²μ 보λ΄κΈ° μ μ μλΉ μμ²μ ν΅ν΄ CORS κ²μ¬λ₯Ό νκ² λ©λλ€. λ°λΌμ, νλ‘ νΈ μ΄ν리μΌμ΄μ
μ΄ λμνλ μ€λ¦¬μ§κ³Ό μλ²(aws ec2)μ μ€λ¦¬μ§μ΄ λ¬λΌ λΈλΌμ°μ μμ μμ api μμ²μ 보λ΄μ§ μκ³ μλ¬λ₯Ό λ°μμν΅λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν΄μλ λ°±μλμμ λ³λμ μ²λ¦¬λ₯Ό ν΄μ£Όμ΄μΌ νκ³ , Spring μμ
WebMvcConfigurer.addCorsMappings(CorsRegistry registry)
λ©μλλ₯Ό μ΄μν΄ μ΄ μ²λ¦¬λ₯Ό νκ² λ©λλ€. κ·Έλ¬λ, μ΄ μ€μ μ΄ μ ν λμνμ§ μλ λ¬Έμ κ° μμμ΅λλ€. - μ΄ νλ‘μ νΈμμ μ¬μ©μμ μΈμ¦,μΈκ° μμ
μ μ²λ¦¬νκΈ° μν΄ Springμμ μ 곡νλ
Interceptor
λ₯Ό μ΄μ©νμ΅λλ€. μΈν°μ ν°λ₯Ό ν΅ν΄ μμ²μ ν€λμ ν¬ν¨λμ΄ μλ μΈμ¦ ν ν°μ μΆμΆνμ¬ μ΄λ₯Ό μ΄μ©ν΄ μ¬μ©μλ₯Ό μΈμ¦ νλ ꡬ쑰μ λλ€. - κ·Έλ¬λ, λΈλΌμ°μ μμ CORS κ²μ¬λ₯Ό μν΄ λ³΄λ΄λ μλΉμμ²(Preflight Request)μλ λΉμ°ν μΈμ¦ν ν°μ΄ ν¬ν¨λμ§ μκ³ , μΈν°μ ν° λ΄λΆμμ NPE(NullPointerException)μ΄ λ°μνκ²λ©λλ€.
- μΈν°μ ν°μμμ μμΈλ°μμ κ²½μ° λ³λλ‘ μ²λ¦¬λ₯Ό νμ§ μμμΌλ―λ‘ μ€νλ§μ΄ μ체μ μΌλ‘ μ²λ¦¬(μλ§λ 500 μλ¬)λ₯Ό νκ²λκ³ μ΄ κ³Όμ μμ Preflight μμ²μ μ λλ‘ μλ΅μ΄ λμ΄κ°μ§ μμλ€κ³ λ³Ό μ μμμ΅λλ€.
- λ°λΌμ μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ μΈν°μ ν°μμ Preflight μμ²μ΄ OPTIONS λ©μλλ‘ μ¨λ€λ μ μ μ΄μ©ν΄ λ³λλ‘ μΈμ¦ μ²λ¦¬λ₯Ό νμ§ μλλ‘ νμ¬ λ¬Έμ λ₯Ό ν΄κ²°νμ΅λλ€.
- 보μμ μ΄μ λ‘ λΈλΌμ°μ μμλ μ£Ό μμ²μ 보λ΄κΈ° μ μ μλΉ μμ²μ ν΅ν΄ CORS κ²μ¬λ₯Ό νκ² λ©λλ€. λ°λΌμ, νλ‘ νΈ μ΄ν리μΌμ΄μ
μ΄ λμνλ μ€λ¦¬μ§κ³Ό μλ²(aws ec2)μ μ€λ¦¬μ§μ΄ λ¬λΌ λΈλΌμ°μ μμ μμ api μμ²μ 보λ΄μ§ μκ³ μλ¬λ₯Ό λ°μμν΅λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν΄μλ λ°±μλμμ λ³λμ μ²λ¦¬λ₯Ό ν΄μ£Όμ΄μΌ νκ³ , Spring μμ
- 볡μ‘νκ³ λ°λ³΅λ μ½λκ° λ§μ΄ 보μ΄λ 컨νΈλ‘€λ¬
-
κ°λ° μ΄κΈ° μ§ν μ€μ 컨νΈλ‘€λ¬ μ½λμ μΌλΆλ₯Ό 보면
@PostMapping("/cert/{goalId:[0-9]+}") public ResponseEntity<Certification> addCertificationByGoalId(@PathVariable long goalId, @RequestBody Certification certification, @RequestHeader("Authorization") String token){ String goalOwnerEmail = JwtBuilder.getEmailFromJwt(token); try { certService.addCert(certification, goalOwnerEmail); return ResponseEntity.ok(certService.getCertificationByGoalId(goalId).orElseThrow()); }catch (PermissionException e){ e.printStackTrace(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); }catch (DuplicateCertificationException e){ e.printStackTrace(); return ResponseEntity.status(HttpStatus.CONFLICT).build(); }catch (DataAccessException e){ e.printStackTrace(); return ResponseEntity.status(HttpStatus.CREATED).build(); } } @PutMapping("/cert/success/{goalId:[0-9]+}") public ResponseEntity<?> successVerification(@PathVariable long goalId,@RequestHeader("Authorization") String token){ String requestEmail = JwtBuilder.getEmailFromJwt(token); try{ verfiService.success(goalId,requestEmail); return ResponseEntity.ok().build(); }catch (DataAccessException e){ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); }catch (PermissionException e){ return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); } }
μ€λ³΅λ μμΈ μ²λ¦¬ μ½λκ° λ§μ΄ 보μ λλ€. μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ μ€νλ§μμ μ 곡νλ
ExceptionHandler
λ₯Ό μ΄μ©νμ΅λλ€. μλλ μ΄λ₯Ό μ΄μ©ν΄ μμΈμ²λ¦¬λ₯ΌSpringHandleExceptionHandler
λΌλ ν΄λμ€λ‘ μ΄κ΄ν λͺ¨μ΅μ λλ€.SpringHandleExceptionHandler
@RestControllerAdvice public class SpringHandleExceptionHandler { @ExceptionHandler(SpringHandledException.class) public ResponseEntity<?> handle(SpringHandledException exception){ exception.printStackTrace(); return exception.parseResponseEntity(); } }
GoalController μΌλΆ
@PostMapping("/cert/{goalId:[0-9]+}") public ResponseEntity<Certification> addCertificationByGoalId(@PathVariable long goalId, @RequestBody Certification certification, @RequestHeader("Authorization") String token){ String goalOwnerEmail = JwtBuilder.getEmailFromJwt(token); try { certService.addCert(certification, goalOwnerEmail); return ResponseEntity.ok(certService.getCertificationByGoalId(goalId)); }catch (DataAccessException e){ e.printStackTrace(); return ResponseEntity.status(HttpStatus.CREATED).build(); } } @PutMapping("/cert/success/{goalId:[0-9]+}") public ResponseEntity<?> successVerification(@PathVariable long goalId,@RequestHeader("Authorization") String token){ String requestEmail = JwtBuilder.getEmailFromJwt(token); try{ verfiService.success(goalId,requestEmail); return ResponseEntity.ok().build(); }catch (DataAccessException e){ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } }
-
μ΄λ² νλ‘μ νΈμλ λ°±μλ κ°λ°μ 1λͺ , νλ‘ νΈμλ κ°λ°μ 3λͺ μ΄ μ°Έμ¬νμ΅λλ€. λ°λΌμ λμ²λλ μμ μμ²κ³Ό κΈ°λ₯μΆκ° μμ²μ νΌμ μ²λ¦¬νλ€λ³΄λ, μ§κΈ μμ±μ€μΈ μ½λκ° λμ€μ λ¬Έμ κ° λ μ μμμ μλ©΄μλ λμ΄κ° λΆλΆμ΄ λλ¬ μμ΅λλ€. κ·Έλ° λΆλΆμ νλμ© μ²μ²ν μμ ν΄λκ° μμ μ λλ€.
- μ΄λ―Έμ§ μ μ₯ μ½λμ λ©μλ λΆλ¦¬
λ± λ΄λ μ½κΈ° μ΄λ €μ΄ μ½λμ λλ€. μ΄λ° μ½λλ€μ΄ νλ‘μ νΈ κ΅¬μꡬμμ μ‘΄μ¬ν©λλ€. μ΄λ° μ½λλ₯Ό μμ ν μμ μ λλ€.
public Announcement addAnnouncement(Announcement announcement) { String image = announcement.getImage(); try { byte[] imageData = java.util.Base64.getDecoder().decode(image.substring(image.indexOf(",") + 1)); String filenameExtension = image.split(",")[0].split("/")[1].split(";")[0]; String fileName = "announcement"+File.separator+announcement.getAnnouncementId(); File imageFile = new File(fileName); BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData)); ImageIO.write(bufferedImage,filenameExtension,imageFile); announcement.setImage(fileName); String banner = announcement.getBannerImage(); announcement.setBannerImage("announcement"+File.separator+"banner"+announcement.getAnnouncementId()); long announcementId = adminRepository.insertAnnouncement(announcement); announcement.setBannerImage(banner); announcement.setAnnouncementId(announcementId); announcement.setDate(Timestamp.valueOf(LocalDateTime.now())); fileName = "announcement"+File.separator+announcement.getAnnouncementId(); imageFile.renameTo(new File(fileName)); System.out.println(fileName); announcement.setImage(""); saveBannerImage(announcement); return announcement; }catch (IllegalArgumentException e){ throw new SpringHandledException(HttpStatus.BAD_REQUEST,ErrorCode.UNKNOWN,"POST /api/admin/announcement","DataURI μ΄λ―Έμ§κ° μλλλ€."); } catch (IOException e) { throw new SpringHandledException(HttpStatus.BAD_REQUEST,ErrorCode.UNKNOWN,"POST /api/admin/announcement","DataURI λ₯Ό νμΌλ‘ λ°κΏ μ μμ΅λλ€."); } }