✂️삭제에 대하여

유저와 같은 중요한 정보를 삭제하려고 할 때,
비밀번호 검증을 통해 본인이 시도를 하고 있는 것인지를 확인하는 과정이 필요하다.

비밀번호는 보안을 위해 파라미터가 아닌 RequestBody를 사용해 넘겨주어야 한다.
그러나 DELETE 메서드는 RequestBody를 사용하지 않는 것이 좋다고 한다.

❓ 그러면 삭제를 무엇으로 하나요?

주로 PATCH를 사용한다.

또는

세션이나 토큰 등을 이용해 비밀번호 검증이 된 상태에서만 삭제를 허용한다.


(PATCH 사용에 대해서는 여전히 고려해야 할 부분이 있다.)


💡 Soft Delete 논리 삭제, Hard Delete 물리 삭제

논리 삭제와 물리 삭제는 삭제를 했을 때, 해당 데이터가 DB에 계속 남아있는가 아닌가의 차이이다.

예를 들어, 네이버 카페가 있다고 가정해 보자.

유저 1이 "test1"이라는 닉네임으로 여러 글을 남긴 후 탈퇴했다.
이후 다른 유저가 "test1"이라는 닉네임으로 카페에서 활동을 한다면
기존 사용자들은 이전 "test1" 유저와 새로운 "test1" 유저의 행동이 달라 혼란을 느낄 수 있다.

따라서 우리는 "test1" 유저 탈퇴 시, 해당 유저를 삭제하지만 닉네임 중복 방지를 위해
"test1"이라는 정보가 여전히 필요하다.

이런 상황에 사용하는 것이 Soft Delete 방식이다.

Soft Delete 논리 삭제

논리 삭제는 실제 데이터를 DB에서 삭제하지 않는다.
다만, is_deleted와 같이 삭제 여부를 나타내는 boolean 변수를 통해 상태를 변경하여
삭제된 것처럼 처리한다.

Hard Delete 물리 삭제

물리 삭제는 DB에서 해당 데이터를 실제로 제거하는 것이다.

PATCH와 DELETE 사용은 비밀번호 검증의 여부가 아닌
삭제 방식에 따라 사용하는 메서드가 달라지고,
주로 PATCH를 사용한다.

위에서 언급했다시피 비밀번호가 이미 인증되었다는 가정하에 DELETE를 사용할 수도 있다.

💡 PATCH 사용 시, 해커를 주의하세요!

대규모 프로젝트에서는 PATCH를 통한 삭제를 권장하지 않는다.

논리 삭제를 하지 않는다는 말이 아니다.
논리 삭제여도 PATCH 메서드 대신 DELETE 메서드를 사용한다는 것이다.

PATCH는 메서드명 자체로 해커에게
"이 기업은 삭제된 정보를 보관합니다!"라는 힌트를 줄 수 있다.

따라서, DELETE 메서드를 사용하는 곳도 있다고 한다.
(회사 규칙에 따라 상이하다.)

이런 부분이 있다 정도로만 이해해도 좋을 것 같다.


🔐비밀번호 변경

유저가 변경 가능한 필드가 다음과 같이 있다고 가정하자.

  1. 닉네임
  2. 소개
  3. 성별 
  4. 생년월일
  5. 비밀번호

이 정보들을 수정하는 API를 만들고자 할 때, API를 어떻게 나누면 좋을까?

📑 각 필드에 따라 변경하는 API 만들기 (👎 비추천 )

필드의 개수가 많아질수록 API 개수가 늘어나기 때문에 좋은 방법이 아니다.

 

♥️ 추천) 유저 정보 변경과 비밀번호 변경을 분리해서 관리

닉네임과 성별처럼 간단하거나, 소개처럼 공백이 허용되는 필드의 변경은
별도의 검증 없이 바로 적용해도 무방한 부분들이다.

이런 필드들은 PUT을 이용해 업데이트하는 방법이 선호된다고 한다!

❓ 변경 가능한 필드를 일부만 변경하고 싶을 때도 PATCH가 아닌 PUT 사용이 가능한가?

가능하다.

닉네임, 소개, 성별, 생년월일 중 소개만 변경하고 싶다고 할 때,
프런트에서 변경된 소개를 제외한 나머지 부분을 기존 값 그대로 입력해 주면 되는 것이다!
{
    "이름" : "현재값",
    "소개" : "변경된 값",
    "성별" : "현재값"
    ...
}

 

위와 같은 방식으로 정보를 업데이트할 때,
비밀번호는 검증과 암호화 같은 비즈니스 로직이 추가된다.

따라서, 비밀번호를 변경하는 API와 비밀번호를 제외한 나머지 유저 정보를 변경하는 API
이렇게 2개로 나누는 방법이 제일 좋은 것 같다! (개인적인 생각)

 

📦 하나의 API로 모든 정보를 수정하기

상황에 따라 다를 것 같다.

1. 비밀번호가 검증되어야만 정보가 변경되도록 하는 경우

정보 변경 시, 비밀번호 검증이 필수라면 나쁘지 않은 방법이라고 생각된다.


2. 변경 시, 비밀번호가 필요 없는 경우

  • PATCH 방식으로 새로운 값이 입력된 필드만 변경되도록 할 때
변경하지 않는 필드의 경우 null이 허용되기 때문에
컨트롤러 계층에서 유효성 검증이 어렵다는 단점이 있다.

컨트롤러 계층에서 @Validate 또는 @Valid로 유효성 검증을 하지 못하는 경우,
직접 클래스를 만들어서 유효성 검증을 해야 한다.

실무에서는 유효성 검증에 다음과 같은 방법을 보편적으로 사용한다고 하니 참고하면 좋을 것 같다.

https://jay-cheol.tistory.com/entry/JAVA%EA%B0%84%EB%8B%A8%ED%95%9C-%ED%8C%A8%EC%8A%A4%EC%9B%8C%EB%93%9C-Validator-%EB%A7%8C%EB%93%A4%EA%B8%B0

 

[JAVA] 정규식을 이용하여 패스워드 Validator 만들기!

패스워드 Validator  Java를 통해서 간단한 패스워드 Validator를 생성해보자. 요즘 보안이 높아짐에 따라 패스워드 규칙도 복잡해진다. 우선 대표적인 규칙들을 나열해보자.  (?=.*[A-Z]): 대문자가

jay-cheol.tistory.com

 

  • PUT 방식으로 새로운 값이 입력되지 않은 필드는 프런트에서 기존 값을 입력할 때
프런트가 비밀번호를 알고 있어야 한다는 점에서 좋지 못한 방법이라고 생각된다.

 


😵 마무리

비밀번호를 다루는 부분에서 들었던 개인적인 고민들과 그에 대한 구글링, 튜터님들께 질문한 결과를
바탕으로 정리를 해보았다.

이 글을 통해 나 같은 고민을 하는 사람들에게 조금이나마 도움이 될 수 있으면 좋겠다.
(To.Me From.Me😜)

개인적인 생각도 많이 들어가 있는데, 잘못된 정보 수정 요청이나 논의는 언제나 환영입니다~😊

📅 일정 관리 프로그램

Spring JPA를 이용해 구현한 일정 관리 프로그램입니다.

일정 관리 프로그램은 아래의 기능들을 제공합니다.

🛠️ 기능

  • 회원가입, 로그인, 로그아웃
  • 유저 이름 및 비밀번호 변경, 삭제
  • 일정 생성, 수정, 삭제
  • 댓글 생성, 수정, 삭제
  • 특정 유저가 작성한 댓글 조회
  • 특정 일정에 작성된 댓글 조회
  • 유저 아이디 또는 수정일에 따른 일정 조회

🛠️ 유저 관련 기능

☑️ 회원가입

이름, 이메일, 비밀번호를 입력해 회원가입을 할 수 있습니다.

API : /users/signup

🔖 예외) 이메일 중복

이때, 이메일은 다른 유저와 중복될 수 없습니다.
만약 이메일이 중복인 경우 해당 이메일로 가입을 진행할 수 없습니다.

이메일 중복 시, 반환되는 상태

🔖 예외) 잘못된 비밀번호 형식

비밀번호는 영문과 숫자의 조합만 가능합니다.

회원가입 유효성 검증

☑️ 로그인

이메일, 비밀번호를 입력해 로그인을 할 수 있습니다.

로그인 성공 시, 유효시간이 30분인 세션이 생성됩니다.

API : /users/login

🔖 예외) 이메일 또는 비밀번호 불일치

🔖 예외) 이미 로그인한 상태인 경우

☑️ 로그아웃 

API : /users/logout

로그아웃하면 유저가 가진 세션이 무효화됩니다.

☑️ 마이페이지

API : /users/me

내 정보를 확인할 수 있습니다.

☑️ 유저 이름, 비밀번호 변경

API : /users/name
API : /users/password

유저 이름과 비밀번호를 별도의 API에서 변경합니다.

유저의 이름은 별도의 비밀번호를 확인하지 않고 쉽게 바꿀 수 있지만
비밀번호는 현재 비밀번호와 새 비밀번호를 입력받고 현재 비밀번호가 일치하지 않으면 변경할 수 없습니다.

🔖 예외) 현재 비밀번호가 불일치

🔖 예외) 변경값이 현재값과 동일

☑️ 유저 삭제

API : /users/me

비밀번호 검증 후, 내 계정을 삭제할 수 있습니다.
계정 삭제 후, 기존 세션이 무효화됩니다.


🛠️ 일정 관련 기능

일정과 댓글의 생성, 수정, 삭제는 세션이 가지고 있는 유저 아이디를 사용해 유저를 식별하기 때문에
일정과 댓글에 필요한 내용만 전달합니다.

✅ 일정 생성

API : /tasks

일정 생성 예제

일정의 제목과 내용을 전달받으면 세션에 의해서 유저 정보가 적용됩니다.

✅ 일정 수정, 삭제

API : /tasks/{id}

일정의 제목 또는 내용을 각각 수정할 수 있습니다.
일정의 수정과 삭제는 본인의 일정만 가능합니다.

🔖 예외) 본인의 일정이 아닐 때 


🛠️ 댓글 관련 기능

☑️ 댓글 생성

API : /tasks/{task_id}/comments

☑️ 댓글 수정, 삭제

API : comments/{id}

댓글의 수정과 삭제는 본인의 댓글만 가능합니다.

🔖 예외) 본인의 댓글이 아닐때


✅ 유저가 작성한 댓글 전체 조회

API : /users/{user_id}/comments

유재석이 작성한 댓글들의 일부

✅ 일정에 속하는 댓글 전체 조회

API : /tasks/{task_id}/comments

핑계고 녹화 일정에 달린 댓글들 (격한 반응으로 하하의 댓글은 삭제됨)

✅ 일정 전체 조회 (유저 아이디, 수정일 조건 설정 가능)

API : /tasks

4월 4일, 하하의 일정 전체 조회

조건에 따른 일정 또는 댓글이 존재하지 않을 때는 0개의 결과가 반환됩니다.

더 자세한 API 명세와 예제는 Postman 문서 링크를 참조해 주세요!

[유저 API] https://documenter.getpostman.com/view/43241868/2sB2cU9NAg
[일정 API] https://documenter.getpostman.com/view/43241868/2sB2cU9NAf
[댓글 API] https://documenter.getpostman.com/view/43241868/2sB2cU9NAe

 

댓글

The Postman Documenter generates and maintains beautiful, live documentation for your collections. Never worry about maintaining API documentation again.

documenter.getpostman.com

 


📈 Entity 연관 관계 with ERD

조회에서 느낄 수 있듯이 유저와 일정, 댓글은 1대 다 연관관계를 가지고 있습니다.

ERD

  • 유저는 0 ~ N개의 일정을 수행할 수 있지만, 일정은 이를 수행할 유저가 없을 수는 없습니다.
  • 유저는 0 ~ N개의 댓글을 작성할 수 있지만, 댓글을 작성한 유저가 없을 수는 없습니다.
  • 일정에는 0 ~ N개의 댓글이 달릴 수 있지만, 댓글은 일정이 없다면 머물 위치가 없습니다.
유저가 삭제되면 유저의 일정과 댓글이, 일정이 삭제되면 해당 일정에 달린 댓글이 삭제되도록
Cascade를 설정했습니다.

📏 주요 구현 방식

💡Session을 이용한 로그인

  1. 클라이언트가 보낸 HTTP 요청을 담은 HttpServletRequest에서 getSession으로 세션을 받아온다
  2. 이때 getSession(false)이면 세션이 없을 때, 새로운 세션을 생성하지 않는다.
  3. 현재 존재하는 세션이 있다면 "이미 로그인된 상태"임을 알린다.
  4. 현재 존재하는 세션이 없다면 getSession(true)로 새로운 세션을 생성한다.
  5. "loginUser"라는 이름의 객체에 사용자 정보를 저장한다.
  6. 세션 유효시간을 설정 후, "로그인 성공"임을 알린다.
    @PostMapping("/login")
    public ResponseEntity<String> login(
            @Valid @RequestBody LoginRequestDto loginRequest,
            HttpServletRequest request
    ){
        HttpSession existingSession  = request.getSession(false);

        //세션이 존재하고 loginUser가 null이 아니라면
        if(existingSession!=null && existingSession.getAttribute("loginUser") != null){
            return ResponseEntity.badRequest().body("이미 로그인된 상태입니다.");
        }

        UserSessionDto loginUser = userService.login(loginRequest);
        //로그인 성공 시, 새로운 세션 생성
        HttpSession session = request.getSession(true);
        //세션에 사용자 정보 저장
        session.setAttribute("loginUser", loginUser);
        //세션 유효시간 30분
        session.setMaxInactiveInterval(30 * 60);
        return ResponseEntity.ok("로그인 성공");
    }

💡@SessionAttribute 사용

세션에 저장된 유저 아이디를 불러와 여러 번 사용하기 때문에
세션이 존재하는지 검증하고, 세션이 존재하는 경우 값을 가져와 사용하는 코드를 반복해서 사용하는 부분이 있었다.

이를 @SessionAttribute로 대체하여 사용하였다.

@SessionAttribute("loginUser")UserSessionDto loginUser

@SessionAttrbute는 세션에서 특정 속성을 가져와 컨트롤러 메서드의 매개변수로 바인딩하는 역할을 하기 때문에
컨트롤러의 메서드에서 간편하게 접근할 수 있다.

💡 비밀번호 암호화

제공받은 암호화 코드를 기반으로 입력받은 비밀번호를 암호화해 저장한다.

String encodedPassword = passwordEncoder.encode(signUpDto.getPassword());

User Entity에서 입력받은 비밀번호와 DB에 저장된 해시된 비밀번호를 BCrypt 해싱 검증을 통해 비교하여
일치 여부를 확인하도록 하였다.

public boolean checkPassword(String password, PasswordEncoder passwordEncoder){
   return passwordEncoder.matches(password, this.password);
}

💡 일정 전체 조회 페이징 + (각 일정의 댓글 개수 필드 추가)

@Query의 사용은 확장성, 자원 소모 등의 이유로 사용하는 것이 좋지 않다고 해서
동적으로 쿼리문을 확장하는 방법이 무엇이 있는지 찾아보다가 Specification을 사용하게 되었다.

유저 아이디가 입력된 경우, user 엔티티의 id의 값이 userId와 같은 것만 조회하도록 한다.
ex) userId = 1일 때 → WHERE user.id = 1

//userId가 null이 아닐 때, Task의 user의 id와 userId를 비교하는 조건 추가
if(userId != null){
    spec = spec.and(((root, query, cb) ->
            cb.equal(root.get("user").get("id"), userId)));
}

 

같은 방식으로 수정일에 대한 조건도 조회한다.
yyyy-mm-dd에 형식으로 비교하기 위해 LocalDate를 사용한다.
ex) updatedAt = "2024-04-01"일 때 → WHERE DATE(updatedAt) = '2024-04-01' 조건이 추가됨.

if(updatedAt != null && !updatedAt.isBlank()){
    //문자열을 LocalDate로 변환 (YYYY-mm-dd)
    //캘린더에서 선택하는 방식을 사용한다는 가정하에 DATE 형식이 맞지 않는다는 가정은 제외함
    //아닐 경우 형식 검사 추가 필요
    LocalDate date = LocalDate.parse(updatedAt, DateTimeFormatter.ISO_LOCAL_DATE);
    //문자열
    spec = spec.and((root, query, cb) ->
            cb.equal(cb.function("DATE", LocalDate.class, root.get("updatedAt")), date));
}


이 부분이 까다로웠다. 
간단한 페이징이었다면 위의 조건에 따라 필터링된 결과를 바로 Page로 반환했을 것이다.
그러나 요구사항에 따라 각 일정에 몇 개의 댓글이 달렸는지 그 개수도 포함해야했다.

@Query의 사용은 별로 권장되지는 않으나 Specification을 사용하면 코드가 오히려 복잡해질 것 같았다.
또 댓글 개수는 일정 전체를 조회할 때 뿐만 아니라 각 일정 별로도 필요했기 때문에 재사용성을 위하여
@Query를 사용해 Repository 계층에 아래와 같은 메서드를 작성해 사용하였다.

    @Query("SELECT c.task.id, COUNT(c) FROM Comment c WHERE c.task.id IN :taskIds GROUP BY c.task.id")
    List<Object[]> countByTaskIds(@Param("taskIds") List<Long> taskIds);
  1. 조회된 일정들의 id를 모은다. 
  2. 모은 Id 리스트를 넘겨 받아 위의 메서드를 이용해 구한 일정의 개수를 반환받아,
    Id에 따른 일정의 개수를 매핑한다.
  3. Task와 댓글 개수를 TaskResponseDto로 변환하여 클라이언트에 반환한다.
 Page<Task> taskPage = taskRepository.findAll(spec, pageable);

 //페이지 내의 일정 id를 모아옴
 List<Long> taskIds = taskPage.getContent().stream()
        .map(Task::getId)
        .toList();

//id에 따른 개수를 매핑
Map<Long, Long> commentCounts = commentRepository.countByTaskIds(taskIds).stream()
        .collect(Collectors.toMap(
                row -> (Long) row[0],
                row -> (Long) row[1]
        ));

//Task와 count로 TaskRespinseDto 리스트 생성
return taskPage.map(task -> {
    Long count = commentCounts.getOrDefault(task.getId(), 0L);
    return new TaskResponseDto(task, count);
});

🤷 트러블 슈팅

@Transactional 사용

🤦‍♀️ 문제 현상

@Transactional을 사용하면 데이터의 변경을 JPA가 자동으로 감지하고 직접 save를 호출하지 않아도
변경사항이 반영되어 DB에 저장된다고 하였다.

그러나 실제로 프로그램 실행 시, 저장이 되지 않았다.
save를 직접 호출해도 저장이 되지 않았다.

✏️ 문제 해결

@Transactional을 제거하니 정상적으로 저장되었다.
(주말동안 배운 내용들을 점검하며 원인을 파악할 예정)


댓글의 개수를 누가 알고 있어야 할까?

🤦‍♀️ 고민한 내용

일정 또는 유저를 반환할 때 일정은 일정에 달린 댓글의 개수를, 유저는 자신이 작성한 댓글의 개수 포함해야했다.

그런데 이 댓글의 개수 엔티티가 계속 가지고 있어야 할까?
아니면 댓글의 개수가 필요할때마다 이를 조회 후 반환해야할까?

이 두 부분이 고민이 되었다.
왜냐하면, 두 방법 모두 좋은 방법은 아니라는 생각이 들었기 때문이다.

댓글의 개수를 엔티티가 가지고 있는 것은 엔티티의 책임 혹은 부담이 늘어나는 것 같고,
필요할 때마다 매번 조회 후 반환하는 것은 자원 소모가 심할 것이라는 생각이 들었다.

✏️ 튜터님의 답변

우선 댓글의 개수는 프론트엔드에서 해당 값을 사용해 페이징을 해야하기 때문에 넘겨줄 필요가 있다. 그리고

댓글의 개수는 필요할 때마다 조회하는 것이 자원 소모가 발생하더라도 훨씬 나은 방법이다!

그 이유는 동시성 제어에 있다!

인스타그램에서 2명이 1개의 피드에 동시에 하트를 눌렀다고 해보자.
댓글의 개수를 Entity가 가지고 있다면 동시성 제어가 어려워
0이던 하트의 개수가 2명이 눌렀음에도 본인이 누른 1만 증가한 결과가 보이게 될 수 있다.

Entity에서 이를 다뤄도 동시성 제어를 할 수 있는 방법이 있긴 있으나 매우 복잡하고 어렵다!
차라리 쿼리를 통해 매번 조회하는 것이 더 효율적이다!

📚 마무리 말

JPA를 처음으로 사용하다보니 아직 어설픈 부분이 많지만,
이번 프로젝트를 통해 어느 정도 감을 잡을 수 있었다.

주말 동안 부족한 부분을 한 번 더 복습하고 정리한 후,
JWT 등 배웠으나 사용하지 않은 방법들도 직접 실습을 해봐야겠다고 생각했다.

(정리할게 산더미...)

DTO

DTO는 Controller - Service - Repository 계층 간 데이터를 전달하는 객체이다.
(Entity에서 필요한 부분만 꺼내서 포장하는 부분이라고 생각할 수 있을 것 같다.)

Entity를 직접 노출하지 않기 때문에 필드를 보호한다.
또한 필요한 필드만 포함하여 성능을 최적화한다.

 

 DTO와 Entity의 차이

항목 DTO Entity
목적 데이터 전달 데이터 저장
사용 범위 Controller - Service - Repository  JPA, 데이터 베이스
캡슐화 비즈니스 로직 없이 데이터만 포함 비즈니스 로직 포함 가능
변경 가능성 API에 따라 유연한 변경 데이터베이스 스키마 변경 필요
설계 사용자(데이터를 사용하는 개발자) 친화적 데이터베이스 친화적

 

DTO에서 Validation으로 검증하지 못한 부분을 서비스 계층에서 검증할 때,
일부 필드에 대한 검증 로직이 반복된다.
이런 경우 Entity에서 이를 처리해주는 것이 좋다.


예를 들어, 비밀번호를 객체를 다룬다고 하자.

DTO에서 Validation을 이용해 입력받은 비밀번호가 몇 글자 이내인지, 형식은 맞는지는 검증할 수 있다.
그러나 입력 받은 비밀번호가 저장되어있는 비밀번호와 일치하는지는 서비스 계층에서 점검해야 한다.

그리고 두 비밀번호를 비교하는 로직은 서비스 계층의 여러 기능에서 반복되서 사용될 수 있다.
이 부분을 Entity에서 구현해 동일한 로직의 반복을 줄일 수 있다.
따라서, Entity에는 비즈니스 로직을 포함하는 것이 가능하다.


😵‍💫 DTO가 너무 많아...!

DTO는 Entity의 변경 없이 API 설계가 가능하고 유지보수와 보안이 용이하기 때문에

기능에 따라 여러 Request, Response DTO를 생성해서 사용한다.

그런데, 테이블이 많아지면? 기능이 많아지면?
그만큼 사용하는 DTO의 개수가 늘어나게 된다.

createADto
createBDto
...

updateADto
updateBDto
...

이런 식으로 말이다.

그렇다면 이를 어떻게 좀 더 간소화해서 관리하는 방법이 없을까?

없다.

공통적인 내용을 구현하고 이를 상속하는 방법을 생각해볼 수는 있다.
그러나 이 방법은 치명적인 단점이 있다.
부모에 변경사항이 생기면 자식들에 대해서 이를 모두 수정해주어야 한다는 수정의 불편함이다.

따라서 DTO의 개수가 늘어나는 것은 어쩔 수 없이 안고 가야하는 부분이다.
다만 많은 DTO를 정리하는 방법은 2가지로 생각해볼 수 있다. (by. 튜터님)

  1. inner class 사용
  2. 기능별 pakage로 분리 

DTO를 설계할 때, 캡슐화를 고려하는 것도 좋지만
DTO로 받은 데이터를 사용자가 어떻게 사용하는지도 중요하다는 점을 유념하며 설계하도록 하자 😉
 

 

🔍 유효성 검사

우리는 Request Body, RequestParam, PathVariable 등의 방식으로 클라이언트가 보낸 데이터를 받아서 처리한다.

이때, 받은 데이터가 잘못된 혹은 허용되지 않는 값이거나 Null 인지 등의 여부를 확인해주어야 한다.

이를 위해 데이터가 유효한 값인지 아닌지를 확인하는 유효성 검사가 필요한다.


🛠️ Spring에 유효성 검사 기능 추가하기

프로젝트에 유효성 검사 기능을 추가하려면 spring-boot-starter-validation 라이브러리를 의존성으로 추가해야 한다.

[ Gradle ]

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

그리고 꼭 Gradle 동기화를 하도록 하자!
(ctrl+s를 한다고 반영이 되는 것이 아니다...)

 


🛠️ 유효성 검사

@Request Body, @RequestParam, @PathVariable로 호출된 API는
@Valid 또는 @Validated을 이용해서 유효성을 검증한다.

✅ @Valid 

  • RequsetBody 앞에 사용한다.
  • DTO에서 유효성을 검증한다.

@RequestBody 앞에 @Valid를 선언한다.

@PostMapping
public ResponseEntity<TaskResponseDto> createTask(@Valid @RequestBody TaskCreateRequestDto createDto)

해당 DTO 내에서 검증이 필요한 필드의 데이터에 대해 어떤 검증을 할지 설정한다.

    @NotBlank(message = "할 일은 필수 설정 값입니다.")
    @Size(max = 200, message = "할 일은 최대 200자까지 입력 가능합니다.")
    private String content;     //할 일

✅ @Validated

  • @RequestParam, @PathVariable, @RequestHeader에서 사용한다.
  • 컨트롤러와 서비스 계층에서 사용할 수 있다.

클래스 외부에 @Validated를 선언하고
검증하고 싶은 데이터의 Annotation 뒤에 검증을 위한 Annotation 추가한다.
당연히 여러 Annotation 사용할 수 있다.

@RestController // @Controller + @ResponseBody
@Validated
//일정 Id에 따른 일정 조회
@GetMapping("/{id}")
public ResponseEntity<TaskResponseDto> findTaskById(@PathVariable @Min(1) Long id) {
    return new ResponseEntity<>(taskService.findTaskById(id), HttpStatus.OK);
}

🟦 @Valid와 @Validation으로 어떤 것들을 검증할 수 있는가?

아래의 표와 같은 Annotation을 이용해 데이터를 검사할 수 있다.

출처 : https://dev-coco.tistory.com/123

💡 NotNull과 NotBlank의 차이

둘의 기능은 유사하지만 조금 다르다.
NotBlank가 NotNull보다 더 빡빡한 검증이다.

NotNull은 값이 존재하기만 하면 검증을 통과한다.
(공백이면 어때.? 어쨌든 데이터가 있긴 한 거니까 통과!)

NotBlank는 null뿐만 아니라 공백들 까지도 허용하지 않는 검증이다.

NotBlank는 데이터 타입이 String일 때 주로 사용한다.


🔨 검증을 통과하지 못한 데이터 처리

검증에 실패했을 때, Spring이 자동으로 예외를 처리하지만,
@ControllerAdvice와 @ExceptionHandler로 커스텀해서 처리할 수도 있다.

💡@ControllerAdvice로

  • 애플리케이션 전역에서 발생하는 예외를 처리한다.
  • 예외 처리할 클래스 위에 @ControllerAdvice를 선언한다.
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
}

보편적으로 ControllerAdvice를 사용할 때는 ExceptionHandler를 함께 사용한다.

💡@ExceptionHandler로 예외 처리

  • 특정 예외를 처리한다. (try-catch의 catch와 같은 역할)
  • @ExceptionHandler(예외클래스명. class)
  • 예외 클래스를 여러 개 선언할 수 있다.

✅ @Valid는 검증 실패 시,  MethodArgumentNotValidException을 반환한다.
@Validated는 검증 실패 시,  ConstraintViolationException을 반환한다.

💡커스텀 예외  처리

예외 클래스만으로 처리하기 어려운 예외를 직접 정의할 수도 있다.

public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

커스텀 예외는 exception 또는 error 패키지를 별도로 만들어 그 안에 선언한다.
커스텀 예외 역시 ExceptionHandler로 ControllerAdvice가 선언된 전역 예외 처리 클래스 안에서 사용할 수 있다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(커스텀예외클래스명.class)
}

🤷 유효성 검사에서 발생한 오류 또는 예외가 여러 개라면?

Map <String, String>을 사용한다.
각 String은 검사에 실패한 필드와 오류 메시지를 저장한다.

 @Valid

 Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
});

getBindingResult()를 통해 검증 실패한 필드들을 가져온다.

 

 @Validated

ex.getConstraintViolations().forEach(violation -> {
    String fieldName = violation.getPropertyPath().toString();
    errors.put(fieldName, violation.getMessage());
});

getConstraintViolations()를 통해 검증 실패한 필드들을 가져온다.

 

💡 FieldError 

FieldError는 필드(변수)에 대한 검증 오류를 나타내는 객체이다.

  •  필드 이름, 기본 메시지, 오류 코드를 가진다.
  • getFieldErrors는 FieldError 객체들의 리스트를 반환한다.

ObjectError를 통해 전체 객체에 대한 오류를 처리할 수도 있다.

 

이렇게 처리된 필드명과 오류 메시지는 JSON 형태로 반환된다.

{
  "userId": "유저 아이디는 필수 설정 값입니다.",
  "content": "할 일은 최대 200자까지 입력 가능합니다."
}

 

BindingResult 직접 사용

BindingResult는 Validation 오류를 보관하는 객체이다.

위의 두 방법은 예외가 발생하면 이를 처리하지만 Binding Result를 직접 사용하면 예외가 발생하지 않는다!  

 

  • Controller 메서드 매개변수에서 직접 사용한다.
  • 개별 컨트롤러에서 유연한 오류 처리가 가능하다는 장점이 곧 단점이다.
    (모든 컨트롤러에서 개별적으로 처리해야 한다.)

 

@PostMapping("/user")
public ResponseEntity<> createUser(@Valid @RequestBody CreateRequestDto createDto, 
					               BindingResult bindingResult) {
    List<ObjectError> allErrors = bindingResult.getAllErrors()
    System.out.println("allErrors = " + allErrors);

    return ResponseEntity.ok("유효한 요청입니다.");
}

 

Controller의 파라미터에 BindingResult를 선언하고
오류를 가져와 확인할 수 있다.

이때 getAllErrors()를 사용하면 필드 오류와 객체 오류를 모두 가져올 수 있고,
getFieldErrors() 또는 getGlobalErrors()를 사용해 필드 오류와 객체 오류를 따로 가져올 수도 있다.

 

❓ 언제 어떤 방법으로 유효성을 검증해야 하지?

BindingResult 직접 사용 getBindingResult()또는 getConstraintViolations() 사용
예외가 발생하지 않는다. 예외가 발생한다.
개별적으로 처리가 가능하다. 글로벌한 예외처리가 가능하다.


컨트롤러에서 유효성 검사를 직접 확인하고 싶다면  BindingResult
모든 검증 오류를 전역으로 일괄 처리하고 싶다면 getBindingResult()또는 getConstraintViolations()

 


😉 예외 처리 이렇게 하면 더 좋다! ( by. 튜터님 )

  • 유효하지 않은 값은 애진작에 통과시키지 않는 것이 좋다!
  • ExceptionHandler로 예외를 예쁘게 포장해 보자
  • Exception을 만들 때, RuntimeException만 사용하는 거보다, 더 자세히 예외를 정해주는 게 좋다.
  • DTO에서 할 수 없는 유효성 검사는 Service 계층보다 Entity 자체에서 검사하도록 하는 것이
    재사용성이 높다!

⚙️ 스케줄러 기능

✅ 유저, 할 일 생성

✅ 유저 및 할 일 조회

✅ 유저 이름 또는 할 일의 내용, 해당하는 유저 수정

✅ 유저 삭제, 할 일 삭제


API 명세서

postman을 활용해 별도의 문서로 정리해 놓았다.

https://documenter.getpostman.com/view/43241868/2sAYkLmcYN


일정과 유저의 관계

일정과 유저는 1:N 관계이다.
일정은 유저의 정보가 필요하지만, 유저에게 할당되어 있는 일정은 없을 수도, 매우 많을 수도 있다.

ERD

유저의 고유 식별자인 user_id를 외래키로 두 테이블이 연결된다.


🗂️ 3 계층 구조

Layered Architecture를 기반으로 Controller, Service, Repository로 계층을 나누어 구현하였다.


💬 트러블 슈팅

1. 자동으로 생성해 준다면서 왜 null이 입력되는 거지?

🤷‍♂️ 현상

프로젝트를 실행했는데 date가 null이라는 오류가 계속 발생했다.
파라미터가 잘못 입력되었나?
해당 부분을 계속 확인하고 혹시나 싶어 더 철저히 수정해 봤지만 오류는 해결되지 않았다.

🔍 문제 발생 위치 원인

원인은 파라미터가 아닌 테이블에 저장된 값에 있었다.
Task 테이블에서 TIMESTAMP 형식으로 저장되는 등록일과 수정일이 null로 기입되어 있어 발생한 오류였다.

테이블 생성 당시, 등록일과 수정일이 자동으로 생성 및 업데이트되도록 설정해 놓았다.
이를 철석같이 믿고 있었기 때문에 해당 부분이 문제라는 점을 찾는데 꽤나 긴 시간이 소요되었다.

💡 원인 및 해결 방법

SimpleJdbcInsert는 자동으로 모든 필드에 대해  insertSQL을 생성한다.
따라서, parameters를 넣지 않은 컬럼에는 null을 넣는다!

위와 같은 SimpleJdbcInsert의 특성에 의해 등록일과 수정일이 자동 생성되는 것이 아닌,
파라미터를 입력하지 않아 null이 넣어지는 것이었다.

usingColums를 통해 사용할 컬럼을 명시하여 해당 문제를 해결하였다.

 

2. API 엔드 포인트를 확인해 주세요!

🤷‍♂️ 현상

조건을 만족하는 일정 전체를 조회하는 기능을 하나의 API에서 수행하고자 할 때,
아래와 같이 코드를 작성했다.
문제가 될만한 부분은 없어 보였다.

실제로 테스트 시
조건이 없을 때, 수정일만 주어졌을 때, 유저 아이디만 주어졌을 때는 정상적으로 동작했다.

그런데 수정일과 유저 아이디가 주어졌을 때는 이를 찾을 수 없다는 결과가 돌아오는 것이었다.

🔍 문제 발생 위치 원인

문제가 발생한 위치는 postman에 api 엔드 포인트 입력 부분이었다. 
tasks 뒤에 /가 들어가며 어떤 API를 실행해야 할지 프로그램이 찾지 못한 것이었다.

💡 원인 및 해결 방법

해당 문제는 API 엔드 포인트를 재설정해주며 간단하게 해결하였다.

 


회고

Spring을 본격적으로 처음 사용하는 것이다 보니 직접 설계 및 구현하고 싶었다.

요구사항을 분석 및 정리하는데 시간이 다소 소요되었고,
예시를 무작정 복붙하기 보다는 한 번씩 더 생각하고 구현하다보니

예외처리 등의 기능까지는 구현하지 못해 아쉬웠다.
그러나 예시가 없어도 구현할 수 있을 정도로 이해도를 높였기 때문에
숙련 주차를 진행하면서 미구현한 부분을 점점 더 보완해 나갈 예정이다. 

챌린지반 코드 리팩토링 시간을 통해 키오스크 과제를 다른 사람들은 어떻게 해결했는지를 알 수 있었다.
해당 코드들이 어떤 면에서 클린하고 좋은 코드인지를 알 수 있어 굉장히 유익했다.
그중에서도 가장 기억에 남는 부분은 빌더 패턴과 유사한 방법의 MenuItem 생성 코드였다.

List<Menu> menu = Arrays.asList(
    new Menu("햄버거").addMenuItems(
		//햄버거 메뉴들...
    ),
    new Menu("음료").addMenuItems(
        //음료 메뉴들...
    ),
    new Menu("사이드").addMenuItems(
		//음료 메뉴들...
    )
);

 //출처 : withong님 github 코드

 

📌 Builder Pattern

객체 생성 시 생성자의 인자가 많을 때 가독성을 높이고, 객체의 불변성을 유지할 수 있도록 도와주는 디자인 패턴

 

가독성 향상: 메서드 체이닝을 활용하여 명확하고 직관적인 객체 생성
불변성 유지: final 필드를 사용하여 객체를 변경할 수 없도록 설정 가능
유지보수 용이: 생성자 오버로딩을 줄이고, 선택적 매개변수를 쉽게 추가 가능
코드의 안정성: 인자의 순서를 실수로 바꿔도 컴파일 타임에서 에러 감지 가능

 

✏️ 생성자 오버로딩의 한계 

public class User {
    private String name;
    private int age;
    private String email;
    private String address;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public User(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    
    //생성자의 매개변수가 많아질수록 늘어나는 생성자...
}
  • 생성자의 매개변수가 많아질수록, 변수 개수에 따라 오버로딩을 계속 추가해야 하는 문제가 발생한다.
    이름만 받는 생성자
    나이만 받는 생성자
    이메일 주소만 받는 생성자
    이름과 나이만 받는 생성자 등...

  • 인자의 순서를 잘못 입력할 가능성도 높아진다.
    (이름이 먼저였나? 나이가 먼저였나?)

빌더 패턴을 사용하면 불필요한 오버로딩을 제거할 수 있다.

public class User {
    private final String name;
    private final int age;
    private final String email;
    private final String address;

    //private 생성자 (Builder만 객체를 생성할 수 있도록 설정)
    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
        this.address = builder.address;
    }

    //Builder 클래스 (User의 내부 정적 클래스)
    public static class Builder {
        private String name;
        private int age;
        private String email;
        private String address;

        public Builder(String name, int age) { // 필수 매개변수
            this.name = name;
            this.age = age;
        }

        public Builder email(String email) { // 선택적 매개변수
            this.email = email;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public User build() { // 최종 객체 생성
            return new User(this);
        }
    }
}

 

위와 같이 빌더를 따로 생성하고 이를 이용해 아래와 같이 메서드 체이닝을 활용해 객체를 생성하게 되면,
Stream을 사용할 때와 유사한 형태로 코드가 작성되며
원하는 매개변수만 선택적으로 설정할 수 있다.

이때, 메서드 체이닝을 사용하려면 빌더의 각 메서드가 this를 반환해야 한다.

public class Main {
    public static void main(String[] args) {
        // 빌더 패턴을 사용하여 User 객체 생성
        User user = new User.Builder("John Doe", 25)
                        .email("john@example.com")
                        .address("New York")
                        .build();
    }
}

 

📌 4. 빌더 패턴 vs 생성자 vs Setter

방식 빌더 패턴 Setter 생성자
가독성 매우 높음⭐⭐⭐⭐⭐ 높음⭐⭐⭐ 낮음⭐
불변성 유지 가능✔️ 불가능❌ 가능✔️
확장성 매우 높음⭐⭐⭐⭐⭐ 높음⭐⭐⭐ 낮음⭐
매개변수 선택 가능✔️ 가능✔️ 불가능❌

생성자, Setter와 성능을 비교했을 때 안 쓸 이유가 없어보이는 성능이다. 


 

빌더 패턴과 유사하게 Menu를 생성할 때 MenuItem을 가변인자를 사용해 추가하는 방식도 인상적이었다.

    public Menu addMenuItems(MenuItem... menu) {
        // 전달된 menu를 리스트로 변환하여 menuItems 리스트에 추가
        this.menuItems.addAll(Arrays.asList(menu));
        // 현재 Menu 객체 반환
        return this;
    }
    
    //출처 : withong님 github 코드

 

📌 가변 인자 Varargs

메서드의 매개변수 개수를 가변적으로 받을 수 있도록 지원하는 기능이다.
배열을 사용하지 않고도 여러 개의 인자를 넘길 수 있다.

✅ 내부 동작

가변 인자는 내부적으로 배열로 변환된다.
따라서, 실제로는 매개변수 타입 배열을 선언한 것과 동일한 의미이다.

public void printNumbers(int... numbers) { }  // 가변 인자

public void printNumbers(int[] numbers) { }   // 배열로 직접 선언

 

✅ 가변인자 사용법

매우 간단하다!
메서드의 매개변수 타입 뒤에 세 개의 점 ...을 붙여서 사용한다.

public void 메서드명(매개변수 타입... 변수명)


가변 인자를 사용할 때, 가변 인자만 매개변수로 받을 수 있는 것은 아니다.
일반 매개변수도 함께 사용 가능하다.

public void 메서드명(일반 매개변수1, 일반 매개변수2, 매개변수 타입... 변수명)
  • 단, 맨 마지막에 위치해야 한다.
  • 가변인자는 한 메서드에 한 번만 사용할 수 있다.
  • 오버로딩 시, 오류가 발생할 가능성이 있다.
public void printNumbers(int... numbers) { }
public void printNumbers(int num) { } // 가변 인자와 혼동될 수 있어 오류 발생 가능

 

 

✅ 오버로딩 발생 시, 해결 방법

  • 다른 타입의 매개변수를 추가해 명확하게 구분한다.
  • 가변 인자 대신 명시적으로 배열을 선언한다.
  • 메서드 이름을 다르게 설정한다.

 

Spring 기초 강의 수강을 마쳤다.
방대한 내용을 흐름과 관계를 중심으로 이해하고 싶어 별도의 글로 작성하였다.

아직 세부적으로 정리할 내용이 많이 남았지만 이렇게 정리하면서 각 요소들이 기억에 잘 남게 된 것 같다.

https://devhippo.tistory.com/94

 

웹 서비스 구조 및 흐름을 비유와 함께 이해하기

웹 서비스의 전체적인 구조와 관계, 흐름, 각 요소의 역할을 좀 더 쉽게 이해하고 떠올릴 수 있도록특정 상황에 비유해 정리하였다.일부 내용은 간략화를 위해 생략되었고 별도의 글에 자세히

devhippo.tistory.com

 


📚 키오스크 프로젝트 피드백

예외를 예외가 아닌 용도로 사용했기 때문에 아이디어는 좋았지만 적절한 방법은 아니라는 피드백을 받았다.

내가 작성한 코드는 아래와 같은 과정으로 구성되어 있다.

While{
    //단계별 처리 메서드 호출
    //다음 단계를 위한 입력받기
} //예외

단계별로 메서드를 호출하는데 이 메서드의 처리 중 일부는 다음 단계를 위한 입력을 받을 필요가 없었다.
메서드 내에서 continue를 사용해, 메서드가 호출된 반복문을 탈출하는 효과를 내어
입력을 받는 단계를 건너뛰고 While문 초기로 돌아가고 싶었다.

이를 위한 방법으로 별도의 예외를 생성해 사용하였다.
이 경우 예외가 발생했다고 가정해 catch 문으로 갔다가 다시 while문으로 돌아오면서
입력 단계를 건너뛰게 된다.


이 방법은 나도 부적절한 방법이 맞다고 생각됐다.
그렇다면 다른 방법은 뭐가 있을지 궁금했다.

👀 이 방법은 어떨까?

튜터님이 재귀, return, Flag 등의 키워드를 제시해 주셨다.

재귀는 내가 짠 알고리즘 상 적용하기 굉장히 까다로울 것 같았다.
다만 return이나 Flag에 대해서는 몇 가지 방법이 떠올랐다.

☝️ Flag 사용

boolean 변수를 사용해서 입력을 받는 상태와 아닌 상태를 구분하기

isGetInput = true

메서드 내 일부 입력을 건너뛰어야 하는 부분
isGetInput = false

if( isGetInput ){
    //입력 단계
}

✌️ return 사용

메서드 내에서 입력을 건너뛰어야 하는 부분 return 0
나머지 return 1

return 된 값이 0이면, continue
아니면, break

 


📚 Getter와 Setter를 사용하면 캡슐화가 깨질 수 있다.

참고하면 좋을 것 같다고 알려주신 글이다.

https://octoping.tistory.com/33

 

사내 세미나 - Getter와 Setter를 함부로 사용하면 안되는 이유;;

들어가기 앞서 지난 번에 작성했던 사내 세미나 - 테스트 코드에 대해 알아보자 세미나의 다음 편으로 진행한 세미나이다. Getter와 Setter의 사용을 금지하라 '리팩토링' 책의 저자로 유명한 Martin F

octoping.tistory.com

캡슐화를 위해 Getter와 Setter를 쓰는 데,
Getter와 Setter가 오히려 캡슐화를 방해하는 아이러니한 상황을 설명하고 있다.
이를 해결하기 위한 자세는 다음과 같다.

  • Lombok의 @Setter 사용 시, 불필요한 것까지 Setter가 생성된다.
  • Setter는 딱 필요한 것만!

 

  • 원시 타입이 아닌 변수들은 Getter로 값을 제공하더라도 이를 변경할 수 있다.
  • 직접 변수의 값을 가져오지 말고 상태를 반환하도록 하자!
삼대중량을 계산하고 싶어!
스쿼트, 벤치프레스, 데드리프트의 수치를 각각 Getter로 가져와서 계산하기
-> 
스쿼트 + 벤치프레스 + 데드리프트를 더한 값을 반환하기


생일을 알고 싶어!
너 몇 월 며칠이 생일이야?
->
너 오늘 생일이야 아니야?

 

굉장히 짧고 굵은 유익한 글이었다.

웹 서비스의 전체적인 구조와 관계, 흐름, 각 요소의 역할을 좀 더 쉽게 이해하고 떠올릴 수 있도록
특정 상황에 비유해 정리하였다.

일부 내용은 간략화를 위해 생략되었고 별도의 글에 자세히 정리하였다.

⚙️ Web Service

웹을 통해 클라이언트와 서버가 HTTP 기반으로 데이터를 주고받으며 동작하는 서비스

웹 서비스 구조는 우리가 일상 생활에서 흔히 볼 수 있는 가게 운영 시스템과 굉장히 유사하다.
다만, 일하는 직원이 전자기기 또는 어떤 개념으로 대체될 뿐이다.


📌클라이언트 - 서버 간 통신

프로토콜에 따라 클라이언트와 서버를 연결하고 데이터를 전송한다.

 

클라이언트와 웹 서버 간의 연결

서로 다른 공간에 있는 고객과 가게는 인터넷으로 연결 되어있다.

IP는 데이터 패킷을 목적지까지 전달하는 배달기사다.
IP 주소와 Port 번호(동호수 같은 역할)로 데이터 패킷을 전송한다.  

이 때, TCP와 UDP와 같은 전송 계층 프로토콜은 배달 방식을 정한다.

TCP와 UDP의 차이를 비유

TCP는 조금 느리지만 신뢰성을 보장하고, UDP는 신뢰성은 낮지만 빠르다.  
https://devhippo.tistory.com/92

 

TCP와 UDP

인터넷상에서는 데이터를 안전하게 전달하기 위한 규칙이 있다.이를 전송 계층 프로토콜이라고 한다. 대표적인 전송 계층 프로토콜에는 TCP와 UDP가 있다.두 방법의 가장 큰 차이는 신뢰성을 고

devhippo.tistory.com

 

이 때, 데이터 패킷은 HTTP Message를 포함하고 있다.
클라이언트는 요청 메세지를 전송하고, 서버는 응답 메세지를 전송한다.
https://devhippo.tistory.com/93

 

HTTP Message

📌 HTTP ( HyperText Transfer Protocol )웹에서 데이터를 주고받기 위한 프로토콜요청과 응답을 주고받는다.✅ 무상태성(Stateless)서버는 클라이언트의 이전 요청 정보를 저장하지 않음이를 보완하기 위

devhippo.tistory.com

 

HTTP Message는 API에서 요구하는 양식에 따라 작성된다.
(API는 데이터를 주고 받는 모든 영역에서 사용된다.)

https://devhippo.tistory.com/58

 

REST API와 RESTful API

API애플리케이션이 서로 통신할 수 있도록 해주는 인터페이스이다.문서 양식이라고 생각하면 이해하기 쉽다.이렇게 요청서를 작성해서 넘겨주면 그에 따라서 필요한 걸 드릴게요!라고 양식에

devhippo.tistory.com


📌Request Message를 받은 Web Server

웹 서버 내부의 일부

주문을 받은 가게는 해당 주문 제품이 완제품(Static Resourse)이면 즉시 이를 배송한다.

만약 완제품이 아닌 조리품이면 내부(WAS)에서 이를 처리한다.
WAS의 직원인 Thread는 받은 주문을 카운터(Servlet)로 들고 간다.

 

🟩 Web Server

HTTP 기반으로 Static Resource(HTML, CSS, JS, 이미지 등)를 제공한다.

대표적인 Web Server로는 Apache와 NGINX가 있다.
둘 모두 따로 호환되는 WAS가 정해져있지는 않다.

 

🟩 Web Application Server

HTTP 기반으로 동작한다. 
Application 로직을 수행하고 DB와 상호작용하여 동적 컨텐츠를 생성한다.

대표적인 WAS로는 Tomcat, Jetty, Undertow가 있다.

Web Server와 Web Application Server는 ScaleOut해 효율적으로 리소스를 관리할 수 있다.
요청에 따라 Thread가 실행되고, Thread는 Servlet 객체를 호출한다.


 

📌Thread의 호출을 받은 Servlet

카운터 Servlet 주문서를 분석, 제품 포장, 배달 요청 등 잡다한 업무들을 처리한다.

다만, Servlet의 업무가 너무 과중했다.
따라서 Servlet의 업무를 분담하기 위해 MVC 패턴을 사용하기로 했다.

//Servlet을 정리한 글 첨부 예정

 

🟩 MVC 패턴

Controller는 요청을 분석 및 분배한다.
Model은 View에 출력할 Data를 저장한다.
View는 Data를 잘 포장한다.
 = View는 Thymeleaf나 JSP와 같은 도구를 사용해 동적으로 HTML을 생성한다.

최근 JSP는 잘 쓰이지 않는 추세이다.

//MVC 패턴 정리글 첨부 예정

🟩 Layered Architecture

MVC 패턴을 사용해도 여전히 Controller의 역할이 너무 많다.
따라서 Controller의 내부를 Layered Architecture로 나눠서 한 번 더 역할을 분배한다.

🟦 Presentation Layer

사용자의 요청을 받고 응답하는 역할을 수행한다.

🟦 Business Layer(Service Layer)

비지니스 로직을 수행한다.

🟦 Repository Layer

DB와 상호작용하며 실제 데이터를 관리한다.

🟦 Persistence Layer

DBMS는 DB를 관리 및 운영하고, JDBC 또는 ORM을 통해 요청에 따른 재료를 전송한다.

//Layered 아키텍처 관련 정리 글 첨부 예정

https://devhippo.tistory.com/99

 

DTO (Data Transfer Object)

DTODTO는 Controller - Service - Repository 계층 간 데이터를 전달하는 객체이다.(Entity에서 필요한 부분만 꺼내서 포장하는 부분이라고 생각할 수 있을 것 같다.)Entity를 직접 노출하지 않기 때문에 필드를

devhippo.tistory.com

 


📌Servlet이 잡무를 다 해주면 개발자는?


요리사(개발자)는 비즈니스 레이어 단계에서 요리를 한다.
Java를 연료로 불을 붙이고, 창고(DB)에서 가져온 재료들을 냄비(Framework)에 넣어
편리한 도구(ex, Spring Boot, Gradle 등)들로 조리한다.

🟩 Framework

소프트웨어 개발을 간편하게 만드는 개발 환경

// Framework 정리글 첨부 예정

🟩 Spring Boot, Gradle

// 관련 정리글 첨부 예정

 

📌조리가 끝나면

주문한 제품이 완성품이냐, 조리품이냐, 밀키트 혹은 비조리품이냐에 따라서
View의 템플릿 엔진이 동적으로 HTML을 생성한다.

SSR과 CSR은 데이터를 포장하는 방식이라고 볼 수 있다.
CSR은 고객에게 밀키트(HTML + JSON)를 전달하고, 고객이 직접(브라우저에서 JavaScript로) 조리(렌더링)한다.

완성된 제품이 Respond Message 함께 고객에게 전달되면, 다리는 사라지고 가게 영업(세션)은 종료된다. 

//SSR과 CSR 관련 정리글 첨부 예정

'Web' 카테고리의 다른 글

쿠키 Cookie와 세션 Session 그리고 토큰 Token  (1) 2025.04.10
DTO (Data Transfer Object)  (0) 2025.03.31
REST API와 RESTful API  (0) 2025.03.20
HTTP Message  (0) 2025.03.18
TCP와 UDP  (0) 2025.03.17

API

애플리케이션이 서로 통신할 수 있도록 해주는 인터페이스이다.

문서 양식이라고 생각하면 이해하기 쉽다.
이렇게 요청서를 작성해서 넘겨주면 그에 따라서 필요한 걸 드릴게요!
라고 양식에 대한 규칙과 명령어를 정의한 것이다.


📌 REST (REpresentational State Transfer) = 자원의 표현에 의한 상태 전달

자원(데이터)을 이름으로 구분, 해당 자원의 상태(데이터 요청 시점의 자원 상태)를 주고받는 모든 것을 의미한다.

HTTP Method로 CRUD Operation을 적용하는 것을 Rest라 칭한다.

🟩 구성 요소

자원 : URI (URL + URN)
표현 : 데이터를 주고 받는 형태, JSON, XML
행위 : HTTP 프로토콜 Method

GET 정보 요청
POST 정보 입력
PUT 정보 업데이트 (데이터 전체를 바꿀 때)
PATCH 정보 업데이트 (데이터 일부만 바꿀 때)
DELETE 정보 삭제 (안전성 문제로 대부분 비활성화)

 

📌REST API

REST의 특징을 기반으로 서비스 API를 구현한 것
각 요청이 어떤 동작이나 정보를 위한 것인지를 요청의 형태로 추론이 가능하다.

🟩 REST API 디자인 가이드 

  1. URI는 정보의 자원을 표현해야 한다.
  2. 자원에 대한 행위는 HTTP Method로 표현한다.
    행위는 URI에 포함하지 않는다.

🟩 REST API 설계 규칙

  • 소문자, 복수명사, 하이픈(-) 사용
  • /로 계층 관계를 표현 (마지막에는 사용해선 안된다.)
  • HTTP 응답 상태 코드 사용 (오류에 대한 피드백)
  • CRUD 함수명 사용하지 않음
  • 파일확장자는 포함하지 않음
  • 정렬, 필터링, 페이징은 신규 API 제작이 아닌 Query Parameter를 사용한다.

📌RESTful API

REST의 원리를 따르는 것을 RESTful 하다고 한다.

 

💡 REST API와 RESTful API의 차이

RESTful API는 REST 아키텍처의 모든 제약 조건을 엄격히 따른다.
반면, REST API는 융통성있게 일부 원칙만 따를 수도 있다.

따라서 모든 RESTful API는 REST API가 될 수 있지만, 반대의 경우는 불가하다.

 

🟩 Restful API 설계 시 고려사항

1. Consumer first
    소비자(또다른 시스템, 개발자) 입장에서 간단하고 직관적인 API를 설계해야 한다.

2. Make best use of HTTP
    HTTP Method 와 Request, Response, Header와 같은 HTTP의 장점을 살려서 개발해야 한다.

3. 성숙도 모델 Level2 이상으로 설계.

4. Response Status
    각각의 API 요청에 따라서 적절한 HTTP 상태코드가 전달되어야 한다.
    왜 실패하고 성공 하였는지 함께 반환시켜주어야 한다.

5. No secure info in URI
    URI 에는 사용자의 정보를 포함해서는 안된다.

6. Use plurals
    제공하는 데이터에 복수형태로 쓰는것이 일반적이다.

7. User nouns for resources
    모든 리소스는 가능하면 동사가 아닌 명사형태로 표시한다.
    API URI 만 보고도 어떠한 API 인지 파악할 수 있는 것이 좋다.

8. For exceptions - define a consistent approach
   일괄적인 엔드포인트를 사용하는것이 좋다.

 

🟩 성숙도 모델

레벨  
Level 0 URL만 매핑한 놓은 상태
Level 1 외부 공개 리소스에 대해 의미있는 URL로 표현
HTTP 별로 서비스를 구분하여 사용하고 있지는 않음
GET, POST로 대부분 요청을 처리하고 에러를 반환한다.
Level 2 - 리소스의 용도와 상태에 따라 HTTP Methods에 맞게 CRUD 설계


- HTTP 의 메소드를 이용하여 리소스의 상태를 구분하여 서비스하면 비슷한 URI라도 메서드에 따라
  다른 형태의 서비스 제공 가능
Level 3 HATEOAS (Hypermedia As The Engine Of Application State)

회원 가입 후 수정 및 조회 등은 어떻게 하는 지, 다음 단계로 진행할 수 있는 또 다른 리소스 정보의 종류 등을 알려주는 기능

- 데이터를 가지고 다음 작업에서 어떤 작업을 할 수 있는지 상태 정보를 함께 넘겨준다
- 클라이언트 측에서 서버가 제공하는 서비스를 일일이 찾는 수고를 겪지 않아도 됨
- 엔드포인트만 가지고 있으면 서버가 제공할 수 있는 다음, 그 다음 URI를 알 수 있다.

 

'Web' 카테고리의 다른 글

DTO (Data Transfer Object)  (0) 2025.03.31
웹 서비스 구조 및 흐름을 비유와 함께 이해하기  (0) 2025.03.20
HTTP Message  (0) 2025.03.18
TCP와 UDP  (0) 2025.03.17
API 간략 정리  (0) 2025.02.17

📌 HTTP ( HyperText Transfer Protocol )

웹에서 데이터를 주고받기 위한 프로토콜
요청과 응답을 주고받는다.

 무상태성(Stateless)

  • 서버는 클라이언트의 이전 요청 정보를 저장하지 않음
  • 이를 보완하기 위해 쿠키(Cookie) / 세션(Session) / 토큰(Token) 사용

비연결성(Connectionless)

  • 클라이언트가 요청을 보내고 서버가 응답하면 연결이 바로 종료
  • 새로운 요청을 보낼 때마다 새로운 연결을 생성해야 함

지속연결 ( Persistent Connection )

비연결성을 개선한 방법으로 한 번의 TCP 연결로 모든 요청과 응답을 주고받은 후 연결을 종료

  • TCP 3-Way HandShake와 연결 종료 횟수 감소
  • 지연 시간이 감소
  • 네트워크 리소스를 절약
  • Keep-Alive 헤더로 일정 시간이 지나면 자동으로 연결을 종료하는 타임아웃 지정 가능

포트(Port) 번호

  • HTTP 포트 80
  • HTTPS 포트 443 

 


📌 HTTP Message 구조

요청 메시지, 응답 메세지 2가지가 있다.

✉️ HTTP 요청 메세지

✅ Request Line

요청 / Path / Version

🗨️ 요청

  • Create - POST
  • Read - GET
  • Update - PUT(전체), PATCH(일부)
  • Delete - DELETE

 

Header

요청과 관련된 부가 정보

🗨️ 필수 요청 헤더

  • Host :  요청할 서버의 도메인
  • User-Agent : 클라이언트의 브라우저, OS 정보
  • Accept : 클라이언트가 받을 수 있는 콘텐츠 타입
  • Connection : 연결 유지 여부 설정

🗨️ 요청 헤더

  • Authorization : 인증 정보 포함
  • Cookie : 클라이언트 쿠키 정보
  • Cache-Control : 캐시 제어 설정

그 외에 엔티티, 보안 관련된 헤더들도 있다.

 

Message Body

POST와 PUT 요청에서 사용된다.
실제 전송하는 데이터가 담긴다.


✉️ HTTP 응답 메세지

Status Line

HTTP  Version / Status Code / Status Text

🗨️ 응답 상태 코드 & 텍스트

🔹 100 (정보 응답)

코드 텍스트 의미
100 Continue 요청이 계속 진행됨
101 Switching Protocols 프로토콜 변경 요청 승인

🔹 200 (성공 응답)

코드 텍스트 의미
200 OK 정상적인 요청 처리 완료
201 Created 요청 성공, 새로운 리소스 생성됨
204 No Content 응답 본문이 없음

🔹 300 (리디렉션)

코드 텍스트 의미
301 Moved Permanently 리소스가 영구적으로 이동
302 Found 리소스가 임시적으로 이동
304 Not Modified 캐시된 데이터가 최신 상태

🔹 400 (클라이언트 오류)

코드 텍스트 의미
400 Bad Request 잘못된 요청
401 Unauthor 인증 필요
403 Forbidden 접근 금지
404 Not Found 요청한 리소스를 찾을 수 없음

🔹 500 (서버 오류)

코드 텍스트 의미
500 Internal Server Error 서버 내부 오류
502 Bad Gateway 게이트웨이 오류
503 Service Unavailable 서버가 과부하 또는 유지보수 중

 

Header

응답 관련 정보

🗨️ 필수 응답 헤더 

  • Date : 필수, 응답이 생성된 시간
  • Content-Type : 필수, 응답 데이터의 타입
  • Server : 서버 소프트웨어 정보
  • Content-Length : 응답 데이터의 바이트 크기

이 외에도 캐시, 제어, 콘텐츠, 보안, 리디렉션 등의 헤더가 있다.

 

Message Body

HTML 또는 JSON 등의 응답 데이터
전송할 데이터가 없다면, 공백 상태이다.


📌 HTTP Method 속성

  • Safe 안전성 
    GET VS POST, DELETE, PUT, PATCH
GET은 그냥 데이터를 읽는 것이지 변환하지 않기 때문에 안전하다.
데이터를 생성, 수정, 삭제하는 메서드는 안전하지 않다.
  • Idempotent 멱등성
    호출 결과는 항상 같다.
    • 복구 매커니즘에 사용
    • 멱등하지 않다면, 중복 요청을 보내선 안됨
    • 재요청 중간에 리소스가 변경되는 것은 멱등성으로 고려하지 않음
GET -> 같은 결과가 계속 조회
PUT -> 수정 후의 결과는 계속 같다
DELETE -> 같은 요청을 여러 번해도 삭제된 결과는 같다
POST -> 멱등성을 보장하지 않는다
  • Cacheable 캐시가능성
    GET, HEAD, (POST)
    • 젼경 가능성이 적은 정적자원을 주로 캐싱한다. 

'Web' 카테고리의 다른 글

DTO (Data Transfer Object)  (0) 2025.03.31
웹 서비스 구조 및 흐름을 비유와 함께 이해하기  (0) 2025.03.20
REST API와 RESTful API  (0) 2025.03.20
TCP와 UDP  (0) 2025.03.17
API 간략 정리  (0) 2025.02.17

+ Recent posts