Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6c6ab14
docs : 7주차 핵심 키워드 작성
kite-pp May 10, 2026
46cda5e
refactor : ddl-auto 수정
kite-pp May 11, 2026
067cc43
refactor : member 도메인 Repository 메소드 수정
kite-pp May 11, 2026
b0f7937
refactor : 페이지네이션 Score:id 수정
kite-pp May 11, 2026
7cd6e5f
docs : Stram API 관련 수정
kite-pp May 11, 2026
e61fb7f
Merge branch 'Gibeom-Week6' into Gibeom-Week7
kite-pp May 11, 2026
ed32523
refactor : 미션 생성시 Void 반환 대신 생성된 미션 정보 반환
kite-pp May 12, 2026
04c0f1d
refactor : ApiResponse 응답 대신 ResponseEntity 래핑 후 반환 처리
kite-pp May 12, 2026
c9f6174
refactor : controller 경로 통일 및 review 커서 페이지네이션 수정
kite-pp May 12, 2026
f4553da
docs : FeedBack 조사
kite-pp May 12, 2026
d3309f9
refactor : 리턴 리팩토링
kite-pp May 16, 2026
1cc4127
feat : 의존성 추가
kite-pp May 16, 2026
54bdcd1
feat : SecurityConfig 추가
kite-pp May 16, 2026
eb834fd
refactor : 안쓰는 에러 삭제
kite-pp May 19, 2026
0cf5246
feat : 회원가입 컨트롤러 추가
kite-pp May 19, 2026
eb99d06
feat : 회원가입 서비스 작성
kite-pp May 19, 2026
a82319d
feat : 회원가입 컨버터 작성
kite-pp May 19, 2026
acbca07
feat : 회원가입 DTO 작성
kite-pp May 19, 2026
1d11a2d
feat : 회원 조회 Boolean 메소드 추가
kite-pp May 19, 2026
0fda854
feat : 회원가입 성공 코드 추가
kite-pp May 19, 2026
8bf2421
feat : 에러코드 추가
kite-pp May 19, 2026
a4c5eb1
feat : SecurityConfig 파일 추가
kite-pp May 19, 2026
719aaab
docs : 8주차 키워드 정리
kite-pp May 19, 2026
db958b5
feat : AuthMember 구현
kite-pp May 19, 2026
558d691
feat : 유저 디테일을 가져오는 서비스 제작
kite-pp May 19, 2026
886eab9
feat : 인증, 인가 오류 처리
kite-pp May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gibeom/umc10th/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testAnnotationProcessor 'org.projectlombok:lombok'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'
Expand Down
80 changes: 0 additions & 80 deletions Gibeom/umc10th/keyword_summary/ch07.md

This file was deleted.

42 changes: 42 additions & 0 deletions Gibeom/umc10th/keyword_summary/ch08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
- Spring Security가 무엇인가?

스프링 기반 애플리케이션의 보안을 담당하는 강력하고 포괄적인 하위 프레임워크

복잡한 보안 로직을 직접 구현할 필요 없이 표준화된 필터 기반의 설정을 통해 시스템을 안전하게 보호한다.

- 인증(Authentication)vs 인가(Authorization)

비슷해보이지만 서로 다른 개념이다.

인증 (Authentication)

- 본인확인 절차
- 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정

인가 (Authorization)

- 권한확인 절차
- 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정

- Stateful vs Stateless

논점 : 서버가 클라이언트의 세션 정보를 기억하는가?

Stateful(상태유지) : 세션 정보를 기억함

Stateless(토큰 기반) : 서버가 상태를 유지하지 않으므로 요청에 포함된 토큰(JWT)로 검증

| 구분 | Stateful | Stateless |
| --- | --- | --- |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stateful/Stateless 비교 표를 작성한 점이 좋습니다. 다만 Markdown 표 구분선의 들여쓰기가 맞지 않아 표가 깨질 수 있으며, 현재 코드의 formLogin은 Stateful 세션 방식에 가깝습니다. 표 아래에 formLogin, SecurityContext, JSESSIONID, JWT 방식의 차이를 현재 구현과 연결해 추가 정리하는 것을 권장합니다.

| 특징 | 서버가 세션 저장소에 로그인 상태 유지 | 서버가 상태를 유지하지 않음, 요청에 포함된 토큰으로 검증 |
| 인증방식 | JSESSION쿠키를 통해 서버 메모리/DB의 세션 조회 | 매 요청시 HTTP헤더에 토큰을 담아서 전송 (Authorization:Bearer<token>) |
| 서버 확장 | 세션 불일치 문제 발생 가능 | 각 요청이 독립적이므로 서버 증설에 유리 |
| 메모리 및 비용 | 동시접속자가 많을수록 서버 세션 메모리 소비 증가 | 토큰 검증 연산이 필요하며, 서버 메모리 사용량은 적음 |
| 주요 활용처 | 전톤적인 웹 애플리케이션 | REST API, 모바일 앱, MSA |

서버 확장 방법

- Scale-up : 단일 서버 성능 향상
- Scale-out : 서버의 개수를 늘리기
- 로드밸런서 : 서버 부하를 분산시키는 H/W, S/W
- 클라이언트와 서버Pool 사이에 위치해 서버의 부하를 분산시키는 하드웨어나 S/W
Original file line number Diff line number Diff line change
@@ -1,54 +1,77 @@
package com.example.umc10th.domain.member.controller;

import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.enums.MissionStatus;
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.domain.mission.dto.MissionResDTO;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.global.entity.AuthMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/members")
@RequestMapping("/api")
public class MemberController {

private final MemberService memberService;
//마이페이지
@GetMapping("/me")
public ApiResponse<MemberResDTO.GetInfo> getInfo(
@GetMapping("/v1/members/me")
public ResponseEntity<ApiResponse<MemberResDTO.GetInfo>> getInfo(
@AuthenticationPrincipal Long memberId
){
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getInfo(memberId));
MemberResDTO.GetInfo result = memberService.getInfo(memberId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge AuthMember에서 회원 id를 꺼내도록 수정하세요

이번 변경에서 CustomUserDetailsService가 인증 principal로 AuthMember를 반환하고, SecurityConfig가 대부분의 API를 인증 필요로 바꿨습니다. 이 상태에서 /v1/members/me@AuthenticationPrincipal Long을 받으면 타입이 맞지 않아 로그인 후에도 memberIdnull로 들어가고 memberService.getInfo(null) 조회가 실패합니다. AuthMember를 주입받아 getMember().getId()를 꺼내거나 @AuthenticationPrincipal(expression = "member.id")처럼 매핑하세요. 다음으로 Spring Security의 UserDetails principal 바인딩 방식을 학습하면 좋습니다.

Useful? React with 👍 / 👎.

return ResponseEntity
.status(code.getStatus())
.body(ApiResponse.onSuccess(code, result));
}

// 홈화면
@GetMapping("/home")
public ApiResponse<MemberResDTO.HomeResultDto> getHome(
@AuthenticationPrincipal Long memberId,
@GetMapping("/v1/members/home")
public ResponseEntity<ApiResponse<MemberResDTO.HomeResultDto>> getHome(
@AuthenticationPrincipal AuthMember authMember,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthMember를 principal로 받는 방향은 좋습니다. 다만 같은 Controller 안에서 다른 API는 아직 @AuthenticationPrincipal Long memberId를 사용하고 있어 인증 주체 타입이 섞입니다. Spring Security가 실제로 주입하는 principal 타입을 하나로 정하고, AuthMember#getMember().getId() 또는 별도 어댑터 방식으로 일관되게 맞추는 것을 권장합니다.

@RequestParam(defaultValue = "0") int page
){
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getHome(memberId, page));
MemberResDTO.HomeResultDto result = memberService.getHome(authMember.getMember().getId(), page);
return ResponseEntity
.status(MemberSuccessCode.OK.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.OK, result));
}

// 진행중/완료 미션 목록 조회
@GetMapping("/missions")
public ApiResponse<List<MissionResDTO.MissionDto>> getMissionsByStatus(
@GetMapping("/v1/members/missions")
public ResponseEntity<ApiResponse<List<MissionResDTO.MissionDto>>> getMissionsByStatus(
@AuthenticationPrincipal Long memberId,
@RequestParam MissionStatus status,
@RequestParam Integer pageSize,
@RequestParam Integer pageNum,
@RequestParam (required = false) String sort
){
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort));
List<MissionResDTO.MissionDto> result = memberService.getMissionsByStatus(memberId, status, pageSize, pageNum, sort);
return ResponseEntity
.status(code.getStatus())
.body(ApiResponse.onSuccess(code, result));
}


//회원가입
@PostMapping("/auth/sign-up")
public ResponseEntity<ApiResponse<Void>> signUp(
@RequestBody @Valid MemberReqDTO.SignUp req
){
memberService.signUp(req);
return ResponseEntity
.status(MemberSuccessCode.CREATED.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.CREATED, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
import com.example.umc10th.domain.member.entity.mapping.MemberMission;

import org.springframework.data.domain.Page;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -44,4 +48,16 @@ public static MemberResDTO.HomeMissionDto toHomeMissionDto(MemberMission memberM
.status(memberMission.getStatus().name())
.build();
}

public static Member toMember(MemberReqDTO.SignUp req, String emcodedPasssword){
return Member.builder()
.name(req.name())
.password(emcodedPasssword)
.phoneNumber(req.phoneNumber())
.email(req.email())
.gender(req.gender())
.userPoint(0)
.nickname(req.nickname())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
package com.example.umc10th.domain.member.dto;

import com.example.umc10th.domain.member.enums.Gender;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

public class MemberReqDTO {

public record SignUp (
@NotBlank
String name,
@NotBlank
String nickname,
@Email @NotBlank
String email,
@NotBlank
String password,
@NotBlank
String phoneNumber,
Gender gender
){}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public class Member extends BaseEntity{
@Column(name = "email")
private String email;

@Column(name = "password", nullable = false)
private String password;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "region_id")
private Region region;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,15 @@ public enum MemberErrorCode implements BaseErrorCode {
"COMMON404_1",
"해당 사용자를 찾을 수 없습니다."
),
EMAIL_DUPLICATED(HttpStatus.NOT_FOUND,
"COMMON404_2",
"해당 사용자를 찾을 수 없습니다."
Comment on lines +15 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 이메일 중복 응답을 Conflict로 분리하세요

signUp에서 existsByEmail이 참이면 EMAIL_DUPLICATED를 던지는데, 현재 코드는 404와 "사용자를 찾을 수 없음" 메시지를 반환합니다. 중복 가입은 리소스를 못 찾은 상황이 아니라 검증 실패/충돌 상황이라 클라이언트가 원인을 잘못 이해하고, 예외 코드 학습에서도 NOT_FOUNDCONFLICT의 책임이 섞입니다. 닉네임 중복처럼 HttpStatus.CONFLICT와 "이미 존재하는 이메일입니다" 메시지로 분리하고, 다음에는 HTTP 상태 코드와 도메인 예외 매핑을 같이 정리해 보세요.

Useful? React with 👍 / 👎.

),
NICKNAME_DUPLICATED(HttpStatus.CONFLICT,
"COMMON409_2",
"이미 존재하는 닉네임입니다."

)
;
private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public enum MemberSuccessCode implements BaseSuccessCode {
"MEMBER200_1",
"성공적으로 유저를 조회했습니다."),

SIGNUP_SUCCESS(HttpStatus.CREATED,
CREATED(HttpStatus.CREATED,
"MEMBER_201_1",
"회원가입이 성공적으로 완료되었습니다.")
;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

public interface MemberMissionRepository extends JpaRepository<MemberMission, Long> {
List<MemberMission> findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus);
Page<MemberMission> findAllByMember_IdAndStatus(Long memberId, MissionStatus missionStatus, Pageable pageable);
Optional<MemberMission> findByMember_IdAndMission_Id(Long memberId, Long missionId);

@Query("SELECT mm FROM MemberMission mm WHERE mm.member.id = :memberId AND mm.status = :status")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,10 @@
import com.example.umc10th.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
Boolean existsByEmail(String email);
Optional<Member> findByEmail(String email);
Boolean existsByNickname(String nickname);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.parameters.P;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
Expand All @@ -28,6 +28,7 @@
public class MemberService {
private final MemberRepository memberRepository;
private final MemberMissionRepository memberMissionRepository;
private final PasswordEncoder passwordEncoder;

public MemberResDTO.GetInfo getInfo(Long memberId) {
Member member = memberRepository.findById(memberId)
Expand Down Expand Up @@ -57,15 +58,26 @@ public List<MissionResDTO.MissionDto> getMissionsByStatus(
} else {
sortInfo = Sort.by("id").descending();
}
// PageRequest 클래스를 사용해 Pageable 객체를 인스턴스화
PageRequest pageRequest
= PageRequest.of(pageNum, pageSize, sortInfo);
PageRequest pageRequest = PageRequest.of(pageNum, pageSize, sortInfo);

List <MemberMission> memberMissions = memberMissionRepository
.findAllByMember_IdAndStatus(memberId, status);
Page<MemberMission> memberMissions = memberMissionRepository
.findAllByMember_IdAndStatus(memberId, status, pageRequest);
List<Mission> missions = memberMissions.stream()
.map(MemberMission::getMission)
.collect(Collectors.toList());
return MissionConverter.toMissionDtoList(missions);
}

public void signUp(MemberReqDTO.SignUp req) {
//닉네임 혹은 이메일이 이미 존재할 때
if(memberRepository.existsByEmail(req.email())){
throw new MemberException(MemberErrorCode.EMAIL_DUPLICATED);
} else if (memberRepository.existsByNickname(req.nickname())){
throw new MemberException(MemberErrorCode.NICKNAME_DUPLICATED);
}

String encodedPassword = passwordEncoder.encode(req.password());
Member member = MemberConverter.toMember(req, encodedPassword);
memberRepository.save(member);
}
}
Loading