정답

  • 프로그램 시작 초기 k일 까지는 명예의 전당에서 가장 작은 값을 ans에 추가
  • k일 이후에는 명예의 전당의 가장 작은 수보다 등록되는 점수가 더 크다면 가장 작은 수를 제외하고 새 점수를 명예의 전당에 등록한 후 명예의 전당 최솟값을 ans에 추가
def solution(k, score):
    best = score[:k]
    m = []
    ans = []
    new_score = score[k:]
    # 프로그램 시작 이후 초기 k일
    for i in best:
        m.append(i)
        m.sort(reverse=True) # 내림차순 정렬
        ans.append(m[-1])

    # k일 이후
    for i in new_score:
        best.sort(reverse=True) # 내림차순 정렬 100.10,1 ...
        if best[-1] < i:
            best.pop()
            best.append(i)
            best.sort(reverse=True)
            ans.append(best[-1])
        else:
            ans.append(best[-1])
    
    return ans

 

첫번째 풀이

  • 나머지를 담는 dum 변수
  • 나머지와 n을 더했을 때 a보다 크다면 다시 콜라로 교환하고 반복문 종료
  • 나머지와 n을 더했을 때 a보다 크다면 다시 콜라로 교환한 후에 다시 반복문을 돌아야 하는 경우가 있을 수도 있으므로 틀림
def solution(a, b, n):
    ans = 0
    dum = 0

    while True:
        if n < a:
            if n + dum >= a:
                n = n + dum
                cola = n // a
                ans += cola * b
                break
            else:
                break
        cola = n // a
        dum = dum + (n % a)
        ans += cola * b
        n = n // a * b
    return ans

 

 

정답

  • 나머지를 담는 변수 제거
  • 이렇게 짜면 나머지를 포함해서 교환을 하고, 이어서 또 교환을 진행 할 수 있을 때 계속 진행할 수 있음
def solution(a, b, n):
    ans = 0
    while True:
        if n < a:
            break;
        ans += (n // a) * b
        n = (n // a) * b + (n % a) 
        
    return ans

 

정답

  • 리스트 역순으로 조회하는 법 : 조회할 리스트[::-1] (시작값, 끝값, 증가값)
  • 음식을 반으로 나눈 리스트를 생성해서 처음부터 끝까지 조회 후 0 입력
  • 0을 입력한 후에 반으로 나눈 리스트를 역순으로 조회
def solution(food):
    ans = ""
    half = []
    idx = 1

    for i in food[1:]:
        half.append(i//2)

    for i in half:
        for j in range(i):
            ans += str(idx)
        idx = idx + 1

    ans += str(0)

    idx = idx - 1
    for i in half[::-1]:
        for j in range(i):
            ans += str(idx)
        idx = idx - 1
        
    return ans

@RequestBody

  • HTTP 요청의 본문(payload) 데이터를 Java 객체로 매핑해줌
  • JSON 데이터를 DTO 객체로 변환

 

위와 같이 요청을 JSON 데이터를 요청 보내면 아래의 객체로 매핑됨!

@PostMapping("/signup")
    public ResponseEntity<SignupResponseDto> signUp(@Valid @RequestBody SignupRequestDto requestDto){
        SignupResponseDto signupResponseDto = authService.signUp(requestDto);
        return new ResponseEntity<>(signupResponseDto, HttpStatus.CREATED);
    }

 

동작 원리

1.  HttpMessageConverter

  • Spring이 HttpMessageConverter를 사용하여 HTTP의 Body를 읽어옴!

2.  Jackson은 객체를 생성하기 위해 기본 생성자를 사용

  • Jackson : JSON 데이터를 Java 객체로 변환하는 라이브러리
  • HttpMessageConverter가 Jackson 라이브러리를 사용하여 JSON 데이터를 Java 객체로 변환

3. 기본생성자로 Dto 객체를 생성한 후에 필드값들은 NULL로 정의되어 있음

4. 리플렉션을 통해서 객체 값 세팅

  • Dto의 내부 필드는 private이지만 리플렉션(reflection)을 통해 객체.필드값 = Http Body와 매칭할 값 으로 필드값을 세팅

 

리플렉션(Reflection)

  • Java의 클래스, 메서드, 필드, 생성자 등의 정보 실행 중(runtime)에 동적으로 탐색하고 조작할 수 있는 기능
  • 캡슐화(private, protected) 필드나 메서드에도 접근이 가능!

 

-> 그러므로 RequestDto는 기본생성자를 사용하고, 필드에는 final을 붙일 필요가 없음!

 

이렇게 작성하면 된다!

@Getter
public class SignupRequestDto {
    private String name;
    private String email;
    private String password;

}

 

하지만 필드를 final로 선언하고 생성자를 따로 만들어도 작동은 됐었다! -> 기본 생성자가 없는 경우 Jackson은 클래스의 파라미터가 있는 생성자를 사용해 객체를 생성 하기 때문이다.

 

결론 : 그래도 앞으로 요청 Dto를 만들 때는 기본생성자 + final 안쓰기 조합으로 만들자!

 

 

 

생성 시간 및 수정시간 관리 : JPA Auditing

  • JPA Auditing : 엔티티의 생성, 수정 시간을 자동으로 기록하고 관리해주는 기능
  • @CreatedDate : 엔티티가 처음 저장될 때 생성 시간 저장
  • @LastModifiedDate : 엔티티가 업데이트될 때 수정 시간 저장 

설정방법

  1. @EnableJpaAuditing를 활성화
  2. @EntityListeners(AuditingEntityListener.class)를 엔티티에 추가
  3. 추상 클래스 필드에 @CreatedDate, @LastModifiedDate 등의 어노테이션 사용
  4. 나머지 객체에서 추상클래스를 extends해서 사용
@EnableJpaAuditing // Auditing 기능(entity 생성, 수정 시간 자동으로 기록) 활성화
@SpringBootApplication
public class ScheduleDevelopApplication {
}
@Getter
@MappedSuperclass // 해당 클래스를 구현했을 때 데이터베이스에 잘 적용되도록 해줌
@EntityListeners(AuditingEntityListener.class) // Auditing 리스너 등록, 생성되거나 수정될 때 값을 업데이트 함
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

 

Comment Entity

  • 생성 및 수정 시간을 기록하는 클래스인 BaseEntity를 상속 
  • Schedule클래스와 Member클래스를 단방향 연관관계로 알고 있음 (어떤 게시물의 댓글인지, 누가 썼는지 알아야 하므로!)
@Getter
@Entity
@Table(name = "comment")
public class Comment extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String content;

    @ManyToOne()
    @JoinColumn(name = "schedule_id")
    private Schedule schedule;

    @ManyToOne()
    @JoinColumn(name = "member_id")
    private Member member;

    @Builder
    public Comment(String content) {
        this.content = content;
    }

    public Comment() {
    }

    public void setSchedule(Schedule schedule){
        this.schedule = schedule;
    }

    public void setMember(Member member){
        this.member = member;
    }

    public void updateComment(String content){
        this.content = content;
    }
}

 

댓글 생성

  • CommentSaveRequestDto : scheduleId, memberId, comment
  • Dto에서 받아온 값으로 Member, Schedule객체를 조회하고 Comment객체를 생성해서 set해준 후 저장함
  • 모든 작업 단위는 @Transactional을 붙여줘야 확실하게 실행 단위 관리가 되기 때문에 붙여줌 (조회는 (readOnly = true))
@Transactional
    public CommentResponseDto save(CommentSaveRequestDto requestDto) {
        Member member = memberRepository.findById(requestDto.getMemberId()).orElseThrow(() -> new MemberNotFoundException());
        Schedule schedule = scheduleRepository.findById(requestDto.getScheduleId()).orElseThrow(() -> new ScheduleNotFoundException());

        Comment comment = Comment.builder()
                                .content(requestDto.getComment())
                                .build();

        comment.setMember(member);
        comment.setSchedule(schedule);

        schedule.addComment(comment);

        Comment savedComment = commentRepository.save(comment);

        return CommentResponseDto.buildDto(savedComment);
    }

 

댓글 조회

  • 댓글 id 값을 통해서 Comment객체 조회
  • 조회이므로 @Transactional(readOnly = true)를 붙여주면 성능이 향상!
@Transactional(readOnly = true)
    public CommentResponseDto findById(Long id) {
        Comment comment = commentRepository.findById(id).orElseThrow(() -> new CommentNotFoundException());
        return CommentResponseDto.buildDto(comment);
    }

 

댓글 수정

  • CommentUpdateRequestDto : comment (댓글 내용)
  • Comment 엔티티 내부에 있는 updateComment 메서드를 사용하여 set 해줌
  • @Transactional을 무조건 명시해 줘야 함! -> JPA의 변경 감지가 작동하는 범위, 명시해주지 않는다면 따로 save()를 해줘야 함!
 @Transactional
    public CommentResponseDto update(Long id, CommentUpdateRequestDto requestDto) {
        Comment comment =  commentRepository.findById(id).orElseThrow(() -> new CommentNotFoundException());
        comment.updateComment(requestDto.getComment());
        return CommentResponseDto.buildDto(comment);
    }

 

댓글 삭제

@Transactional
    public void delete(Long id) {
        Comment comment =  commentRepository.findById(id).orElseThrow(() -> new CommentNotFoundException());
        commentRepository.delete(comment);
    }

 

일정 페이징 조회

ScheduleEntity

  • 조회할 데이터 중에서 "댓글의 개수"가 있었기 때문에 Comment와 Schedule을 양방향 연관관계로 설정
  • addComment 메서드를 Comment를 추가
@Getter
@Entity
@Table(name = "schedule")
public class Schedule extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private String content;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "schedule", fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();

    @Builder
    public Schedule(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public Schedule() {
    }

    public void setMember(Member member) {
        this.member = member;
    }

    public void updateSchedule(String title, String content){
        this.title = title;
        this.content = content;
    }

    public void addComment(Comment comment){
        this.comments.add(comment);
    }
}

 

구현

  • page, size를 쿼리 파라미터로 받음
@GetMapping()
    public ResponseEntity<Page<SchedulePageResponseDto>> findAll(@RequestParam(value = "page", defaultValue = "0") int page,
                                                                 @RequestParam(value = "size", defaultValue = "10") int size){
        Page<SchedulePageResponseDto> scheduleResponseDto = scheduleService.findAll(page, size);
        return new ResponseEntity<>(scheduleResponseDto, HttpStatus.OK);
    }

 

  • repository에 Page타입으로 리턴받는 조회 메서드 추가
@Repository
public interface ScheduleRepository extends JpaRepository<Schedule, Long> {
    Page<Schedule> findAll(Pageable pageable);
}

 

  • PageRequest 를 통해서 page, size를 입력
  • Page<Schedule>로 받아온 객체를 map함수를 통해서 Page<SchedulePageResponseDto>로 변경
  • 회원명과 댓글개수는 schedule엔티티 내부에있는 각각의 엔티티에서 조회해 옴
@Transactional(readOnly = true)
    public Page<SchedulePageResponseDto> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size);
        Page<Schedule> schedulePage = scheduleRepository.findAll(pageable);
        return schedulePage.map(schedule -> {
            Member member = schedule.getMember();
            Long commentCount = (long) schedule.getComments().size();
            return SchedulePageResponseDto.builder()
                    .title(schedule.getTitle())
                    .content(schedule.getContent())
                    .author(member.getName())
                    .commentCount(commentCount)
                    .createdAt(schedule.getCreatedAt())
                    .modifiedAt(schedule.getModifiedAt())
                    .build();
        });
    }

 

  • Application에 @EnableSpringDataWebSupport를 추가하면 Page객체에서 핵심 내용만 조회할 수 있음
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) // Page 객체에서 필요한 부분만 조회할 수 있음
@SpringBootApplication
public class ScheduleDevelopApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleDevelopApplication.class, args);
    }

 

결과

요청 : http://localhost:8080/schedules?page=1&size=3

예외처리 및 검증

에러 응답 객체

@Getter
public class CommonErrorResponse {

    private String status;
    private String message;
    private int code;


    public CommonErrorResponse(HttpStatus status, String message){
        this.status = status.name();
        this.code = status.value();
        this.message = message;
    }

}

 

예외 처리 클래스

  • RuntimeException을 상속한 ApplicationException을 생성
@Getter
public class ApplicationException extends RuntimeException {
    private final HttpStatus status;

    public ApplicationException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}
  • 나머지 커스텀 예외 처리 클래스는 모두 ApplicationException를 상속
public class InvalidPasswordException extends ApplicationException{
    public InvalidPasswordException() {
        super("비밀번호가 일치하지 않습니다.", HttpStatus.UNAUTHORIZED);
    }
}

 

예외처리 클래스들

 

ExceptionHandler

총 3가지 경우의 ExceptionHandler를 만듦

 

런타임 에러 발생 핸들러 (ApplicationException.class)

  • RuntimeException을 상속한 ApplicationException을 상속한 예외가 발생했을 때 처리해주는 핸들러
  • CommentNotFoundException(존재하지 않는 댓글), InvalidPasswordException(비밀번호가 틀린 경우) 등등의 예외처리
@ExceptionHandler(ApplicationException.class)
    public ResponseEntity<CommonErrorResponse> handleApplicationException(ApplicationException e){
        CommonErrorResponse errorResponse = new CommonErrorResponse(e.getStatus(), e.getMessage());
        log.error("[ApplicationExceptionHandler] ApplicationException = {}, class = {}", e.getMessage(), e.getClass());
        return new ResponseEntity<>(errorResponse, e.getStatus());
    }

 

Dto 검증이 잘못된 경우에 발생하는 에러를 관리하는 핸들러 (handleValidationException)

  • 요청값이 객체로 반환된 후에 @Valid를 통한 검증에 걸리는 경우의 에러를 처리하는 핸들러
  • @NotBlank, @Email 등을 안 지킨 경우
@ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<CommonErrorResponse> handleValidationException(MethodArgumentNotValidException e) {
        String firstErrorMessage = null;
        for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
            firstErrorMessage = fieldError.getDefaultMessage();
            break;
        }

 

바인딩 실패한 경우 발생하는 에러를 관리하는 핸들러 (handleHttpMessageNotReadableException)

  • @RequestBody는 HTTP Body Data를 Object로 변환하므로 변환이 실패된다면 Controller를 호출하지 않음
  • 객체 바인딩이 실패한 경우 (Long타입에 String을 넣어서 전송한 경우)
@ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<CommonErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException e){
        CommonErrorResponse errorResponse = new CommonErrorResponse(HttpStatus.BAD_REQUEST, "객체 바인딩이 실패하였습니다. 타입을 다시 확인해주세요.");
        log.error("[HttpMessageNotReadableExceptionHandler] HttpMessageNotReadableException = {}, class = {}", e.getMessage(), e.getClass());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }

 

구현하면서 알게 된 점

아래 두개 때문에 시간을 좀 잡아먹었다 🙄

  • @NotBlank : String에만 사용해야 함, Long타입에는 @NotNull을 사용해야 함!
  • @Size : 문자열 길이에 사용하는 것, @Range는 숫자에 사용하는 것!

 

비밀번호 암호화

  • BCrypt 라이브러리 설치 후 스프링 빈에 등록
  • BCrypt.MIN_COST : 암호화를 할 때 사용하는 비용, 적을수록 성능이 높아지지만 암호화의 수준은 낮아짐
@Component
public class PasswordEncoder {

    public String encode(String rowPassword){
        return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rowPassword.toCharArray());
    }

    public boolean matches(String rawPassword, String encodedPassword) {
        BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
        return result.verified;
    }
}

 

  • encode : 암호화를 수행하는 함수
  • 암호화 수행 후에 데이터베이스에 비밀번호 저장
@Transactional
    public SignupResponseDto signUp(SignupRequestDto requestDto) {
        String encodedPassword = passwordEncoder.encode(requestDto.getPassword());

        Member member = Member.builder()
                        .name(requestDto.getName())
                        .email(requestDto.getEmail())
                        .password(encodedPassword)
                        .build();

        Member savedMember = memberRepository.save(member);

        return SignupResponseDto.buildDto(savedMember);
    }

 

  • matches : 암호화하기 전 값과 암호화한 후의 값을 비교하는 함수
  • 사용자가 입력한 비밀번호와 데이터베이스에 암호화된 비밀번호를 비교
if (passwordEncoder.matches(rawPassword, member.getPassword())){
            return new LoginResponseDto(member.getId());
        }

 

 

댓글 연관관계를 추가한 ERD

고민했던 점

  • 처음에 ERD를 설계할 때 댓글에 회원 FK가 들어가서 테이블이 사이클 형태가 만들어져서 잘 설계한 것인지 고민됨
  • 일정 테이블에도 FK로 회원ID를 가지고 있는데 굳이 댓글 테이블에도 회원 FK를 넣어야 하나 고민했음
  • 하지만 생각해보니 일정은 "일정 작성자 식별자"만 가지고 있고, 댓글은 "댓글 작성자의 식별자" 를 가지고 있는 것이기 때문에 추가하는 것이 맞았음 -> 짱구가 쓴 일정에 짱구아빠 흰둥이가 댓글을 달 수 있기 때문에 엄연히 다르다!  

 

SQL

CREATE TABLE member
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL COMMENT '회원 식별자',
    name       VARCHAR(200) NOT NULL COMMENT '이름',
    email      VARCHAR(50) NOT NULL COMMENT '이메일',
    password   VARCHAR(50) NOT NULL COMMENT '비밀번호',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일'
);

CREATE TABLE schedule
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '일정 식별자',
    member_id  BIGINT COMMENT '작성자 식별자',
    title      VARCHAR(200) NOT NULL COMMENT '제목',
    content    TEXT NOT NULL COMMENT '일정 내용',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일',
    FOREIGN KEY (member_id) REFERENCES member(id)
);

CREATE TABLE comment
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '댓글 식별자',
    member_id  BIGINT COMMENT '작성자 식별자',
    schedule_id  BIGINT COMMENT '일정 식별자',
    content    TEXT NOT NULL COMMENT '댓글 내용',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일',
    FOREIGN KEY (member_id) REFERENCES member(id),
    FOREIGN KEY (schedule_id) REFERENCES schedule(id)
);

 

API

 

DTO

  • 로그인, 일정, 회원, 댓글 부분으로 총 4개의 역할에 따라 분리

 

세션 로그인 구현

1. 로그인 로직 실행

  • LoginRequestDto : 사용자로부터 이메일과 비밀번호를 입력받음
  • 사용자로부터 입력받은 비밀번호가 데이터베이스에 저장된 암호화된 비밀번호랑 동일한 경우, LoginResponseDto에 사용자 id값을 넣어서 반환
public LoginResponseDto login(LoginRequestDto requestDto){
        String rawPassword = requestDto.getPassword();
        Member member = memberRepository.findByEmail(requestDto.getEmail()).orElseThrow(() -> new MemberNotFoundException());
        if (passwordEncoder.matches(rawPassword, member.getPassword())){
            return new LoginResponseDto(member.getId());
        } else {
            throw new InvalidPasswordException();
        }
    }

 

2. HttpServletRequest를 받아서 session에 sessionKey 입력해 주기

  • HttpServletRequest : 클라이언트가 보낸 HTTP 요청 정보(헤더, 파라미터, 세션 정보 등)를 담고 있는 객체
  • 로그인이 성공했다면 클라이언트의 세션에 ( "loginUser": "사용자pk값") 형식으로 sessionKey 추가해서 응답
  • 키 값은 상수로 관리 (반드시 인터페이스나 추상클래스로 관리)
  • 세션ID는 쿠키를 통해서만 전송되도록 application.properties에 아래와 같이 설정 (설정하지 않는다면 최초 로그인시에 세션 ID값이 URL로 나옴)
server.servlet.session.tracking-modes=cookie
@PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto loginRequestDto, HttpServletRequest servletRequest){
        LoginResponseDto responseDto = authService.login(loginRequestDto);
        HttpSession session = servletRequest.getSession();
        session.setAttribute(Const.LOGIN_USER, responseDto);
        return new ResponseEntity<>(responseDto, HttpStatus.OK);
    }

 

  • 로그인이 성공한 경우에 세션ID값이 쿠키를 통해서 전송됨

 

3. Filter 생성하기

고민했던 점

  • 세션 활성화 여부를 확인할 때 조건에서 session.getId()를 사용했더니 로그인 검증이 제대로 되지 않았음
    • 세션 ID는 세션이 활성화 됐을 때만 존재하는 아이디인줄 알았음
    • 알고보니 HttpSession 객체가 생성되면 서버에서 세션 ID를 부여하고 이 값은 세션이 활성화되지 않거나 필요한 속성이 저장되지 않아도 반환되는 값이라고 함
    • 세션 ID가 존재한다고 해서 세션에 유효한 데이터가 있다는 보장 x
  • session.getAttribute("sessionKey")의 존재여부로 검증해서 해결 
    • 사용자가 로그인이 성공했다면 로그인시에 세션에 ( "loginUser": "사용자pk값") 형식으로 sessionKey 추가해서 응답했기 때문에 이 값이 존재하는지 확인하는 것으로 로그인을 했는지 확인 가능
    • 세션이 만료되거나 로그아웃 되면 해당 값은 존재하지 않음
    • sessionKey가 존재한다면 세션에 유효한 데이터가 있다는 보장 o
 if (!isWhiteList(requestURI)){
            // 세션이 존재하지 않는다면 생성하지 않음 (= 로그인을 하지 않았다는 의미)
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null || session.getAttribute("loginUser") == null) {
                throw new UnauthorizedException();
            }
        }

 

코드 설명

  • WHITE_LIST : 필터를 거치지 않아도 되는 인가가 필요없는 경로 리스트
  • ServletRequest는 기능이 적기 때문에 HttpServletRequest로 다운캐스팅
  • chain.doFilter(request,response) 이후에 다음 필터가 있다면 그 필터로 가고, 없다면 바로 Servlet 호출
@Slf4j
public class LoginFilter implements Filter {
    public static final String[] WHITE_LIST = {"/auth/signup", "/auth/login", "/auth/session"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // ServletRequest는 기능이 적기 때문에 HttpServletRequest로 다운캐스팅
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String requestURI = httpServletRequest.getRequestURI();

        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        log.info("로그인 필터 실행");

        // 검증을 제회하는 URI에 포함되지 않은 경우 검증 실행
        if (!isWhiteList(requestURI)){
            // 세션이 존재하지 않는다면 생성하지 않음 (= 로그인을 하지 않았다는 의미)
            HttpSession session = httpServletRequest.getSession(false);
            if (session == null || session.getAttribute("loginUser") == null) {
                throw new UnauthorizedException();
            }
        }

        // 더 이상 호출할 필터가 없으면 Servlet 바로 호출
        chain.doFilter(request,response);
    }

    private boolean isWhiteList(String requestURI) {
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}

 

4. Filter 적용하기

  • FilterRegistrationBean : 스프링 부트에서 필터를 등록하고 설정할 때 사용하는 클래스
  • 우선순위는 숫자가 적을수록 높음
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}

 


추가 +) 해설 세션을 듣고 추가한 내용 ^^;;

생각해보니 memberId값을 로그인을 수행한 후에 세션 속성에 넣었기 떄문에, 요청을 보낼 때 세션에 있는 해당 memberId 속성값을 사용해서 사용해야 하는데, 이걸 빼먹어 버렸다 🙀 (세션의 역할이 사용자 식별 이건데 ...ㅎㅎㅎㅎ)

 

세션 로그인을 통한 인가

  • 로그인 성공시에 세션에 지정해 줬던 값
session.setAttribute(Const.LOGIN_USER, responseDto.getId());
  • 세션에 저장된 loginUser값을 꺼내서 어떤 회원의 요청인지 확인할 때 사용!
  • @SessionAttribute : 세션에 저장된 데이터를 컨트롤러에서 사용할 수 있게 하는 역할
  • 이제 요청 Dto에는 userId가 없음!
@PatchMapping()
public ResponseEntity<ScheduleResponseDto> update(@RequestBody @Valid ScheduleUpdateRequestDto requestDto,
                                                  @SessionAttribute(name = Const.LOGIN_USER) Long userId){
    ScheduleResponseDto scheduleResponseDto = scheduleService.update(userId, requestDto);
    return new ResponseEntity<>(scheduleResponseDto, HttpStatus.OK);
}

API 명세서

  • 수정이나 삭제는 로그인이 성공한 후에 진행한다고 가정하고 만들었기 때문에 따로 요청 Dto에 비밀번호를 넣지 않음

 

Dto 목록

  • 생성, 수정 등 역할에 따라 Dto를 분리함
  • 크게 일정, 회원, 로그인 파트 3가지로 나눔

 

ERD

  • 회원은 여러개의 일정을 작성할 수 있기 때문에 회원 : 일정 = 1 : N

 

테이블 생성 SQL

CREATE TABLE member
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY NOT NULL COMMENT '회원 식별자',
    name       VARCHAR(200) NOT NULL COMMENT '이름',
    email      VARCHAR(50) NOT NULL COMMENT '이메일',
    password   VARCHAR(50) NOT NULL COMMENT '비밀번호',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일'
);

CREATE TABLE schedule
(
    id         BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '일정 식별자',
    member_id  BIGINT COMMENT '작성자 식별자',
    title      VARCHAR(200) NOT NULL COMMENT '제목',
    content    TEXT NOT NULL COMMENT '일정 내용',
    created_at TIMESTAMP NOT NULL COMMENT '작성일',
    updated_at TIMESTAMP NOT NULL COMMENT '수정일',
    FOREIGN KEY (member_id) REFERENCES member(id)
);

+ Recent posts