Skip to content

πŸ–₯ 골킀퍼 ν”„λ‘œμ νŠΈμ˜ λ°±μ—”λ“œ νŒŒνŠΈμž…λ‹ˆλ‹€.

Notifications You must be signed in to change notification settings

Goal-achievement-system/backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

λͺ©ν‘œλ‹¬μ„± μ‹œμŠ€ν…œ λ°±μ—”λ“œ

πŸ€™ μ½”λ”© μ»¨λ²€μ…˜

νŒ¨ν‚€μ§€λͺ…, 클래슀λͺ…은 일반적인 κ΄€λ‘€λŒ€λ‘œ μž‘μ„±ν–ˆμ–΄μš”.

일반적인 λ³€μˆ˜λŠ” 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λ₯Ό μ‚¬μš©ν–ˆμ–΄μš”.

πŸ›  이슈

  1. νŠΈλžœμ μ…˜ μ„€μ • 적용 μ•ˆλ¨
    • λͺ‡λͺ‡ κΈ°λŠ₯의 경우 보닀 μž‘μ€ κΈ°λŠ₯의 μ‘°ν•©μœΌλ‘œ μ΄λ£¨μ–΄μ§€λŠ”λ°, 각각의 μž‘μ€ κΈ°λŠ₯μ—μ„œ μ–΄λ–€ 였λ₯˜κ°€ λ°œμƒν•œ 경우 λͺ¨λ“  μž‘μ€ κΈ°λŠ₯듀이 μ‹€ν–‰λ˜κΈ° μ΄μ „μœΌλ‘œ μƒνƒœλ₯Ό λ˜λŒλ €μ•Ό ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ μŠ€ν”„λ§μ—μ„  @Transactional μ–΄λ…Έν…Œμ΄μ…˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, 이 섀정이 μ œλŒ€λ‘œ μ μš©λ˜μ§€ μ•ŠλŠ” ν˜„μƒμ΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    • ν•΄λ‹Ή μ–΄λ…Έν…Œμ΄μ…˜μ΄ 적용된 λ©”μ„œλ“œ λ‚΄λΆ€μ—μ„œ try-catch문이 λ™μž‘ν•˜λŠ” 경우 νŠΈλžœμ μ…˜ μ–΄λ…Έν…Œμ΄μ…˜μ΄ μ œλŒ€λ‘œ λ™μž‘ν•˜μ§€ μ•ŠλŠ” κ²½μš°κ°€ μžˆλ‹€λŠ” 정보λ₯Ό μ°Ύμ•˜κ³ , 이λ₯Ό μ μš©ν•΄ ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€.
  2. CORS μ˜ˆμ™Έμ²˜λ¦¬ μ„€μ • 적용 μ•ˆλ¨
    • λ³΄μ•ˆμ˜ 이유둜 λΈŒλΌμš°μ €μ—μ„œλŠ” μ£Ό μš”μ²­μ„ 보내기 전에 μ˜ˆλΉ„ μš”μ²­μ„ 톡해 CORS 검사λ₯Ό ν•˜κ²Œ λ©λ‹ˆλ‹€. λ”°λΌμ„œ, ν”„λ‘ νŠΈ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ λ™μž‘ν•˜λŠ” μ˜€λ¦¬μ§„κ³Ό μ„œλ²„(aws ec2)의 μ˜€λ¦¬μ§„μ΄ 달라 λΈŒλΌμš°μ €μ—μ„œ μ•„μ˜ˆ api μš”μ²­μ„ 보내지 μ•Šκ³  μ—λŸ¬λ₯Ό λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ°±μ—”λ“œμ—μ„œ λ³„λ„μ˜ 처리λ₯Ό ν•΄μ£Όμ–΄μ•Ό ν•˜κ³ , Spring 에선 WebMvcConfigurer.addCorsMappings(CorsRegistry registry) λ©”μ„œλ“œλ₯Ό μ΄μš”ν•΄ 이 처리λ₯Ό ν•˜κ²Œ λ©λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, 이 섀정이 μ „ν˜€ λ™μž‘ν•˜μ§€ μ•ŠλŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    • 이 ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©μžμ˜ 인증,인가 μž‘μ—…μ„ μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ Springμ—μ„œ μ œκ³΅ν•˜λŠ” Interceptor λ₯Ό μ΄μš©ν–ˆμŠ΅λ‹ˆλ‹€. 인터셉터λ₯Ό 톡해 μš”μ²­μ˜ 헀더에 ν¬ν•¨λ˜μ–΄ μžˆλŠ” 인증 토큰을 μΆ”μΆœν•˜μ—¬ 이λ₯Ό μ΄μš©ν•΄ μ‚¬μš©μžλ₯Ό 인증 ν•˜λŠ” κ΅¬μ‘°μž…λ‹ˆλ‹€.
    • κ·ΈλŸ¬λ‚˜, λΈŒλΌμš°μ €μ—μ„œ CORS 검사λ₯Ό μœ„ν•΄ λ³΄λ‚΄λŠ” μ˜ˆλΉ„μš”μ²­(Preflight Request)μ—λŠ” λ‹Ήμ—°νžˆ 인증토큰이 ν¬ν•¨λ˜μ§€ μ•Šκ³ , 인터셉터 λ‚΄λΆ€μ—μ„œ NPE(NullPointerException)이 λ°œμƒν•˜κ²Œλ©λ‹ˆλ‹€.
    • μΈν„°μ…‰ν„°μ—μ„œμ˜ μ˜ˆμ™Έλ°œμƒμ˜ 경우 λ³„λ„λ‘œ 처리λ₯Ό ν•˜μ§€ μ•Šμ•˜μœΌλ―€λ‘œ μŠ€ν”„λ§μ΄ 자체적으둜 처리(μ•„λ§ˆλ„ 500 μ—λŸ¬)λ₯Ό ν•˜κ²Œλ˜κ³  이 κ³Όμ •μ—μ„œ Preflight μš”μ²­μ— μ œλŒ€λ‘œ 응닡이 λ„˜μ–΄κ°€μ§€ μ•Šμ•˜λ‹€κ³  λ³Ό 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
    • λ”°λΌμ„œ 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μΈν„°μ…‰ν„°μ—μ„œ Preflight μš”μ²­μ΄ OPTIONS λ©”μ„œλ“œλ‘œ μ˜¨λ‹€λŠ” 점을 μ΄μš©ν•΄ λ³„λ„λ‘œ 인증 처리λ₯Ό ν•˜μ§€ μ•Šλ„λ‘ ν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν–ˆμŠ΅λ‹ˆλ‹€.
  3. λ³΅μž‘ν•˜κ³  반볡된 μ½”λ“œκ°€ 많이 λ³΄μ΄λŠ” 컨트둀러
    1. 개발 초기 진행 μ€‘μ˜ 컨트둀러 μ½”λ“œμ˜ 일뢀λ₯Ό 보면

      @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 λ₯Ό 파일둜 λ°”κΏ€ 수 μ—†μŠ΅λ‹ˆλ‹€.");
            }
        }
    λ”± 봐도 읽기 μ–΄λ €μš΄ μ½”λ“œμž…λ‹ˆλ‹€. 이런 μ½”λ“œλ“€μ΄ ν”„λ‘œμ νŠΈ ꡬ석ꡬ석에 μ‘΄μž¬ν•©λ‹ˆλ‹€. 이런 μ½”λ“œλ₯Ό μˆ˜μ •ν•  μ˜ˆμ •μž…λ‹ˆλ‹€.

About

πŸ–₯ 골킀퍼 ν”„λ‘œμ νŠΈμ˜ λ°±μ—”λ“œ νŒŒνŠΈμž…λ‹ˆλ‹€.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages