Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6917905
스프링 입문 실습 1차 업로드
yun1code Oct 6, 2025
26504a1
SEOYEON_1 실습 기록 README 추가
yun1code Oct 7, 2025
8feaece
README.md 파일 위치 수정
yun1code Oct 7, 2025
77c3669
스프링 입문 실습 2차 업로드
yun1code Oct 12, 2025
db2f63b
README.md 2주차 실습기록 및 느낀점 추가
yun1code Oct 12, 2025
9be9a09
README.md파일 2주차 markdown 수정
yun1code Oct 12, 2025
bd7cf4f
스프링 입문 실습 3주차 업로드
yun1code Nov 3, 2025
274251a
스프링 활용 실습 3주차 업로드
yun1code Nov 3, 2025
85341d1
week3: Update README.md
yun1code Nov 3, 2025
cd3b4ef
스프링 활용 실습 4주차 업로드
yun1code Nov 11, 2025
d43b550
Merge remote-tracking branch 'origin/SEOYEON_2' into SEOYEON_2
yun1code Nov 11, 2025
7bffb21
Update README.md
yun1code Nov 11, 2025
1fc4e5a
스프링 활용 실습 5주차 업로드
yun1code Nov 17, 2025
91804d0
Merge branch 'SEOYEON_2' of https://github.com/25-26-GDGoC-SSWU/Sprin…
yun1code Nov 17, 2025
ce774ff
스프링 활용 실습 5주차(+폴더 구조 재정비)
yun1code Nov 17, 2025
ede4782
Update README.md
yun1code Nov 17, 2025
90d1ae2
update: [week5] README.md
yun1code Nov 17, 2025
7b45ae5
스프링 활용 실습 6주차(완)
yun1code Nov 24, 2025
0ab9ca8
Merge branch 'SEOYEON_2' of https://github.com/25-26-GDGoC-SSWU/Sprin…
yun1code Nov 24, 2025
29c9822
feat: Member, Post 엔티티 생성
yun1code Jan 1, 2026
4f0701c
feat: MemberRepository, PostRepository 추가
yun1code Jan 1, 2026
125bef8
feat: 게시글 요청, 응답 DTO 추가
yun1code Jan 1, 2026
f78e22f
feat: 게시글 생성, 조회 서비스 로직 구현
yun1code Jan 1, 2026
ea47a44
feat: 게시글 생성, 단건 조회 API 추가
yun1code Jan 1, 2026
5aef370
chore: MySQL 및 JPA 설정(비밀번호 제외)
yun1code Jan 1, 2026
2cfd2f6
feat: Spring Security 설정 추가
yun1code Jan 9, 2026
34e3c23
fix: Member 생성자에서 createdAt 초기화
yun1code Jan 9, 2026
7559c5b
feat: 회원가입 및 로그인 Controller 구현
yun1code Jan 9, 2026
c5afbc6
feat: 요청 DTO에 Validation 어노테이션 적용
yun1code Jan 9, 2026
bbff051
feat: 공통 예외 처리 구조 추가
yun1code Jan 9, 2026
2b80bec
feat: MemberRepository 이메일 조회 관련 메서드 추가
yun1code Jan 9, 2026
1cea2d5
feat: 회원가입 및 로그인 비즈니스 로직 추가
yun1code Jan 9, 2026
270a0b3
fix: 게시글 조회 시 공통 예외 처리 적용
yun1code Jan 9, 2026
8a35a18
chore: Security 및 Validation 의존성 추가
yun1code Jan 9, 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
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,58 @@
# Spring
Spring Study Repository
# Spring Study 기록

## 📚 실습 내용
**1주차**
- 스프링 입문 강의 섹션2 ~ 섹션4 (간단한 회원 관리 및 테스트) 실습 완료
- 스프링 프로젝트 생성 및 구조 이해
- `@BeforeEach`를 사용하여 간단한 DI 실습, `@AfterEach`를 사용하여 테스트 실습


**2주차**
- 스프링 입문 강의 섹션5 ~ 섹션7 실습 완료
- 회원 웹 기능(회원 등록, 회원 목록 조회) 실습
- H2 데이터베이스를 이용해 메모리가 아닌 DB에 데이터 영구적 저장
- 스프링 통합 테스트 실습 및 단위 테스트 학습


**3주차**
- 스프링 입문 강의 섹션8 실습 완료
- AOP를 사용했을 때와 사용하지 않았을 때의 함수 호출 시간 측정 방법의 차이 학습
- 공통 관심 사항과 핵심 관심 사항에 대해 학습
- 스프링의 AOP 동작 방식 학습
- 스프링부트와 JPA 활용 강의 섹션2, 섹션3(~10강) 실습 완료
- 프로젝트 설정, 라이브러리, View 환경 설정, H2 데이터베이스 설치, JPA와 DB설정 및 동작 확인
- 간단한 쇼핑몰 요구사항 분석, 도메인 모델과 테이블 설계 학습


**4주차**
- 스프링부트와 JPA 활용 강의 섹션3(11강~), 섹션4, 섹션5 실습 완료
- 엔티티 클래스 개발과 엔티티 설계 시의 주의점 학습 (연관관계 지연로딩 설정 등)
- 구현 요구사항 및 애플리케이션 아키텍처 학습 및 정리
- 회원 도메인 개발 (리포지토리, 서비스) 및 테스트 (메모리 DB 사용)


**5주차**
- 스프링부트와 JPA 활용 강의 섹션6, 섹션7, 섹션8(27강) 실습 완료
- 상품 엔티티 개발, 주문 엔티티 개발, 홈 화면과 레이아웃 설정 실습
- 생성 메서드, Dirty Checking, 도메인 모델 패턴 개념 학습


## ✏️ 느낀 점
**1주차**
이번 실습을 통해 실무에서는 테스트를 어떻게 진행하는지, 스프링의 MVC 구조는 어떻게 작동하는지 감을 잡을 수 있었다.


**2주차**
jdbc, jdbc template, JPA, 스프링 데이터 JPA 순으로 학습하여 기술이 발전하며 단점을 보완해나가는 것을 느꼈고, 최근 뿐만 아니라 이전에 작업하던 방식도 학습하여 예전에는 개발을 얼마나 힘들게 진행했었는지 조금이나마 알게되었다.


**3주차**
AOP를 이용해 원하는 곳에 공통 관심 사항을 적용할 수 있음을 알게 되었고, 실무에서는 핵심적인 비즈니스 로직뿐만 아니라, 시간 측정과 같이 비즈니스 로직 외적인 부분의 처리도 중요하다는 것을 알게 되었다.


**4주차**
`@Entity` 애노테이션을 이용하여 엔티티 클래스를 개발하는 방법을 알게되었고, 엔티티를 설계할 때의 중요한 주의점들을 알게되어 단순히 개발 뿐만 아니라 설계 단계에서도 주의해야 함을 깨닫게 되었다. 또한 회원 도메인을 직접 개발해보면서 실무에서는 어떤 식으로 개발하고 테스트 하는지를 알게되었다.


**5주차**
복잡한 생성에 있어서는 이를 생성 메서드로 별도로 처리해야 한다는 것을 알게 되었고, 이로 인해 개발 시에는 유지보수의 중요성이 크다는 것을 깨닫게 되었다. 또한 테스트할 때에는 테스트 코드 자체뿐만이 아니라 어떤 테스트가 이루어져야 하는건지 정확하게 정의하는게 중요하다는 것을 알게 되었다.
42 changes: 42 additions & 0 deletions firstpractice/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.9'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.study'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0'
implementation 'org.springframework.boot:spring-boot-starter-security' // BCrypt
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.study.firstpractice.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

//Swagger 접속을 위해 추가한 코드입니다...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. CSRF 보호 비활성화 (POST 요청 허용을 위해 필수)
.csrf(csrf -> csrf.disable())

// 2. CORS 설정 (선택사항이나 추가 권장)
.cors(cors -> cors.disable())

// 3. 모든 요청에 대해 인증 없이 접근 허용
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
)

// 4. 기본 로그인 폼 및 HTTP Basic 인증 비활성화
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable());

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.study.firstpractice.controller;

import com.study.firstpractice.dto.LoginRequestDto;
import com.study.firstpractice.dto.LoginResponseDto;
import com.study.firstpractice.dto.SignupRequestDto;
import com.study.firstpractice.dto.SignupResponseDto;
import com.study.firstpractice.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;

@PostMapping("/signup")
public SignupResponseDto signup(@Valid @RequestBody SignupRequestDto request){
return memberService.signup(request);
}

@PostMapping("/login")
public ResponseEntity<LoginResponseDto> login(@Valid @RequestBody LoginRequestDto request){
memberService.login(request);
return ResponseEntity.ok(new LoginResponseDto("로그인 성공"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.study.firstpractice.controller;

import com.study.firstpractice.dto.PostRequestDto;
import com.study.firstpractice.dto.PostResponseDto;
import com.study.firstpractice.service.PostService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController {
private final PostService postService;

@PostMapping()
public PostResponseDto createPost(@RequestBody PostRequestDto requestDto){
Copy link
Collaborator

Choose a reason for hiding this comment

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

지금 PostController.createPost()에서 @Valid가 빠져 있어서 실제 검증이 동작하지 않습니다...! DTO에는 잘 넣어두셔서, 컨트롤러에 @Valid만 추가해주면 서연님이 의도하신 대로 동작할 수 있습니다! 다음에는 꼭 컨트롤러단에 @Valid를 넣어주세요!

return postService.createPost(requestDto);
}

@GetMapping("/{postId}")
public PostResponseDto getPost(@PathVariable Long postId){
return postService.getPost(postId);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.study.firstpractice.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

@Column(nullable = false, unique = true)
private String email;

@Column(nullable = false)
private String password;

@Column(nullable = false)
private String nickname;

@Column(nullable = false)
private LocalDateTime createdAt;

public Member(String email, String password, String nickname) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.createdAt = LocalDateTime.now(); //null 에러 방지용
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.study.firstpractice.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;

@Column(nullable = false)
private String title;

@Column(nullable = false)
private String content;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Column(nullable = false)
private LocalDateTime createdAt;

@Column(nullable = false)
private LocalDateTime updatedAt;

public Post(String title, String content, Member member) {
this.title = title;
this.content = content;
this.member = member;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.study.firstpractice.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class LoginRequestDto {

@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;

@NotBlank(message = "비밀번호를 입력해주세요.")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.study.firstpractice.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class LoginResponseDto {
private String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.study.firstpractice.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostRequestDto {

@NotBlank(message = "제목은 필수로 입력해주세요.")
@Size(min = 2, max = 50, message = "제목은 2자 이상 50자 이하로 입력해주세요.")
private String title;

@NotBlank(message = "내용을 입력해주세요.")
private String content;

@NotNull(message = "memberId는 필수입니다.")
private Long memberId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.study.firstpractice.dto;

import com.study.firstpractice.domain.Post;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
public class PostResponseDto {
private Long postId;
private String title;
private String content;
private Long memberId;
private LocalDateTime createdAt;

public static PostResponseDto from(Post post) {
return new PostResponseDto(
post.getId(),
post.getTitle(),
post.getContent(),
post.getMember().getId(),
post.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.study.firstpractice.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;

@Getter
public class SignupRequestDto {

@NotBlank(message = "이메일은 필수 입력 항목입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;

@NotBlank(message = "비밀번호는 필수 입력 항목입니다.")
@Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
private String password;

@NotBlank(message = "닉네임은 필수 입력 항목입니다.")
private String nickname;
}
Loading