
이번 세미나는 단순히 기능을 구현하는 걸 넘어서
설계와 구조, 그리고 통신의 본질까지 짚어가는 시간이었다.
객체지향, 예외 처리, SOLID, 동시성, HTTP/REST, Spring Boot
한 주제씩 제대로 이해하고 넘어가는 게 훨씬 더 중요하다는 걸 느꼈다.
기계적인 코딩에서 벗어나 본질을 다시 학습하는 것의 중요성을 느꼈고 결정적으로 재밌었다.
Entity와 Domain은 코드 상으로는 거의 동일하게 생겼지만,
역할과 해석이 전혀 다르다.
@Entity 어노테이션을 통해 영속성 컨텍스트에 의해 관리됨.equals(), hashCode()는 ID 기반으로, getter/setter 위주 구조.@Entity
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
// Getter, Setter, equals, hashCode ...
}
public class PostTitle {
private final String value;
public PostTitle(String value) {
if (value == null || value.length() > 30) {
throw new IllegalArgumentException("제목은 30자 이하여야 합니다.");
}
this.value = value;
}
public String getValue() {
return value;
}
}
👉 Entity는 데이터 저장에 집중, Domain은 의미와 제약을 갖는다.
유효성 검사는 크게 두 가지 관점으로 나뉜다.
ControllerService// DTO의 유효성 검사
public record PostCreateRequest(
@NotBlank(message = "제목은 필수입니다.")
@Size(max = 30, message = "제목은 30자를 넘을 수 없습니다.")
String title
) {}
Controller 단에서 @Valid를 통해 요청 자체의 형식을 검증하고,
이후 Service 단에서는 중복 여부 같은 도메인 룰을 검증한다.
public void createPost(String title) {
if (postRepository.existsByTitle(title)) {
throw new DuplicateTitleException("이미 존재하는 제목입니다.");
}
// ...
}
결국 "책임의 위치"가 중요한 것이다.
"Controller에서 new Post 하면 안 돼요?"
물론 간단한 경우엔 가능하다. 하지만…
→ Service가 Domain을 조립해야 한다.
public Post createPost(PostCreateRequest request) {
User user = userRepository.findById(request.userId())
.orElseThrow(() -> new EntityNotFoundException("User not found"));
PostTitle title = new PostTitle(request.title());
Post post = new Post(title, user);
return postRepository.save(post);
}
즉, Entity 생성은 도메인 조립의 일부이며, Controller는 그 책임을 가지면 안 된다.
| 종류 | 대표 클래스 | 특징 |
|---|---|---|
| Checked | IOException, SQLException | 반드시 try-catch 또는 throws 필요 |
| Unchecked | NullPointerException, IllegalArgumentException | 컴파일러 강제 없음 |
CheckedException은 복구 가능한 예외,
UncheckedException은 대부분 개발자 실수나 로직 버그다.
try-catch → 외부 자원 작업 (파일, 네트워크)throw → 도메인 룰 위반throws → 책임 전가 (라이브러리 메서드 등)try {
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
String line = br.readLine();
} catch (IOException e) {
throw new CustomFileReadException("파일을 읽는 중 에러 발생", e);
}
서비스가 죽지 않고 살아남으려면, 모든 예외 흐름을 통제할 수 있어야 한다.
하나의 클래스는 하나의 이유로만 변경되어야 한다
❌ 나쁜 예
public class PostService {
public void createPost(String title) {
int id = postId++;
Post post = new Post(id, title);
postRepository.save(post);
}
}
✅ 개선 예
public class IdGenerator {
private AtomicInteger id = new AtomicInteger(1);
public int generate() {
return id.getAndIncrement();
}
}
확장엔 열려 있고, 수정엔 닫혀 있어야 한다
❌ if문에 의존
if (paymentMethod.equals("card")) { ... }
else if (paymentMethod.equals("kakao")) { ... }
✅ 다형성으로 확장
interface Payment {
void pay();
}
class CardPayment implements Payment { public void pay() {...} }
class KakaoPay implements Payment { public void pay() {...} }
추상화에 의존해야 한다 (구체 클래스 X)
public class PostService {
private final PostRepository repository;
public PostService(PostRepository repository) {
this.repository = repository;
}
}
→ 나중에 Spring의 DI 컨테이너가 이 역할을 대신 해준다.
부모 객체는 자식 객체로 대체 가능해야 한다
예외를 던지는 자식은 위험하다.
class FilePostRepository implements PostRepository {
public void save(Post post) {
throw new UnsupportedOperationException("지원하지 않는 기능");
}
}
이런 구현체는 대체 불가능하며, LSP를 위반한다.
인터페이스는 클라이언트가 사용하는 메서드만 가져야 한다
interface FullRepository {
void save(); void migrate(); void syncToS3();
}
→ 일부 구현체는 불필요한 메서드를 억지로 구현해야 함
→ 작고 명확한 인터페이스로 나누는 게 이상적이다
java
복사편집
private int postId = 1;
public void createPost(String title) {
int id = postId++;
// 동시 요청 시, 동일한 ID가 할당될 수 있음
}
➡ 이건 전형적인 Race Condition이다.
synchronized, AtomicInteger, DB Auto-Increment 등private final AtomicInteger postId = new AtomicInteger(1);
public void createPost(String title) {
int id = postId.getAndIncrement();
}
공유 자원에는 락이 필요하다.
POST /posts HTTP/1.1
Content-Type: application/json
{
"title": "Spring은 언제 배워도 어렵다"
}
GET /posts)Content-Type)자원을 명확하게 식별하고, 상태를 표현하며, 일관된 인터페이스를 사용하는 아키텍처 스타일
- URI는 명사형 (
/posts/1)- 메서드는 행위 표현 (
GET,POST,PUT,DELETE)- 응답은 상태 코드로 구체적으로 표현 (
200,201,404,500)
HTTP/1.1 201 Created
Location: /posts/1
REST는 단순한 스타일이 아니라 웹을 어떻게 설계할 것인가에 대한 철학이다.
@RestController로 HTTP 엔드포인트 연결@RequestBody → JSON → DTO@Service, @Repository → 책임 분리@Entity → JPA 기반 ORM 매핑Spring은 우리가 배운 모든 개념을 실제 웹 애플리케이션으로 연결시켜주는 프레임워크이다.
이번 세미나는 단순히 코드를 배우는 게 아니라 코드를 바라보는 눈을 키우는 시간이었다.
객체를 만들고, 역할을 나누고, 설계를 고민하고, 통신을 설계하고,에러를 통제하고, 사용자와 대화하는 방식까지.