[강의] Spring MVC practice 1

Jerry·2025년 8월 6일

controller

@Controller와 @RestController의 차이

  • @Controller HTML 뷰 페이지를 반환할 때 사용 (Model 객체로 데이터 전달)
@Controller
public class PageController {
    @GetMapping("/hello")
    public String helloPage(Model model) {
        model.addAttribute("message", "Hello Spring!");
        return "hello"; // templates/hello.html
    }
}
  • @RestController JSON 형식의 데이터를 반환할 때 사용 (API 서버에서 사용)
@RestController
public class ApiController {
    @GetMapping("/api/hello")
    public String helloApi() {
        return "Hello API!";
    }
}

@RequestMapping, @GetMapping, @PostMapping

  • @RequestMapping("/users") 공통 URL 경로를 지정
  • @GetMapping, @PostMapping 각각 GET / POST 요청을 처리할 메서드 지정
    • 중복 가능: @GetMapping({"/", "/list"})
@Controller
@RequestMapping("/users")
public class UserController {

    @GetMapping("/signup")
    public String showSignupForm() {
        return "signup-form";
    }

    @PostMapping("/signup")
    public String handleSignup(User user) {
        // 회원가입 처리
        return "signup-success";
    }

    @GetMapping({"/", "/list"})
    public String userList() {
        return "user-list";
    }
}

데이터 받는 방식

  • Form 방식 (User user 또는 String name 등으로 자동 매핑)
@PostMapping("/signup")
public String handleSignup(User user) {
    System.out.println(user.getUsername());
    return "signup-success";
}
  • JSON 방식 (@RequestBody User user → JSON Body를 파싱)
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
    return ResponseEntity.ok(user);
}

PathVariable vs RequestParam

  • @PathVariable URL 경로 안에서 변수 추출
  • @RequestParam 쿼리 파라미터 (?id=1) 방식
// /users/123
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
    return "User ID: " + id;
}

// /users/find?id=123
@GetMapping("/find")
public String findUser(@RequestParam Long id) {
    return "User ID: " + id;
}

모델에 데이터 전달

@GetMapping("/profile")
public String userProfile(Model model) {
    model.addAttribute("name", "홍길동");
    return "profile";
}
  • 뷰 템플릿에서 ${name}으로 접근 가능

리다이렉트

@PostMapping("/delete")
public String deleteUser(Long id) {
    // 삭제 로직
    return "redirect:/users/";
}
  • 브라우저에게 다른 URL로 이동하라고 지시하는 방식
  • 주로 POST 요청 후 GET 페이지로 이동할 때 사용

예외 처리

@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
    return ResponseEntity
        .internalServerError()
        .body("서버 오류가 발생했습니다: " + ex.getMessage());
}
}
  • 예외 발생 시 JSON 형태로 메시지 응답 가능

기타 개념

  • @RequiredArgsConstructor final 필드 기반 생성자 자동 생성 (롬복)
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

→ 생성자 자동 생성

  • @ResponseBody 문자열이나 객체를 그대로 HTTP 응답 Body로 반환
@GetMapping("/text")
@ResponseBody
public String plainText() {
    return "그냥 텍스트로 응답합니다";
}
  • ResponseEntity 응답의 본문 + HTTP 상태 코드를 함께 보낼 때 사용
@GetMapping("/api/status")
public ResponseEntity<String> apiStatus() {
    return ResponseEntity.status(200).body("OK");
}

domain

import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class User {
    private Long id;
    private String username;
    private String password;
    private String name;
}

주요 어노테이션 설명

어노테이션설명
@DataGetter, Setter, toString, equals, hashCode, RequiredArgsConstructor 자동 생성
@AllArgsConstructor모든 필드를 파라미터로 받는 생성자 생성
@NoArgsConstructor파라미터 없는 기본 생성자 생성
@Builder빌더 패턴 적용 (메서드 체인 방식으로 객체 생성 가능)

event, listner

package com.codeit.start.event;

import com.codeit.start.domain.User;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class UserRegisteredEvent extends ApplicationEvent {
    private final User user;

    public UserRegisteredEvent(Object source, User user) {
        super(source); // 이벤트의 발생 주체
        this.user = user;
    }
}
package com.codeit.start.listener;

import com.codeit.start.event.UserRegisteredEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

// UserRegisteredEvent 이벤트가 발생하면 이벤트를 받을 Listener
@Component // 해당 Class를 일반 Bean으로 등록하는 어노테이션
public class UserEventListener {

    @EventListener // 이벤트가 발생하면 실제 처리할 메서드 (call back 함수, handler, Listener)
    public void onApplicationEvent(UserRegisteredEvent event) {
        String username = event.getUser().getUsername();

        // 메일 전송하는 예시
        System.out.println("[메일 전송] " + username + "님이 가입하였습니다." );
        System.out.println("[메일 전송 완료]");
    }
}
  • 사용자가 회원가입에 성공했을 때 발생하는 사용자 정의 이벤트
  • Spring에서 제공하는 ApplicationEvent를 상속받아 만들며 이벤트 리스너(@EventListener)를 통해 다른 컴포넌트에 알릴 수 있습니다.

필수 구성요소 설명

요소설명
extends ApplicationEventSpring 이벤트 객체로 만들기 위한 상속
Object source이벤트를 발생시킨 주체 (보통 this)
User user이벤트와 함께 전달할 데이터 (가입된 사용자 정보)
@GetterLombok으로 user에 대한 getter 자동 생성

사용 예시

이벤트 발행

@Autowired
private ApplicationEventPublisher eventPublisher;

public void register(User user) {
    // ... 사용자 저장 로직
    eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
}

이벤트 리스너

@Component
public class WelcomeEmailListener {

    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        // 이메일 발송 로직
        System.out.println("📧 환영 이메일 전송: " + user.getUsername());
    }
}

정리

  • ApplicationEvent를 상속하여 커스텀 이벤트 클래스를 만들 수 있다.
  • 이벤트 객체는 필요한 데이터를 함께 보낼 수 있다 (User 등).
  • ApplicationEventPublisher로 발행하고, @EventListener로 수신 처리한다.
  • 관심사 분리(Separation of Concerns)에 매우 유용

service

UserService

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public User register(User user) {
        User saveUser = userRepository.save(user);

        // 회원가입 완료 후 이벤트 발행!
        eventPublisher.publishEvent(new UserRegisteredEvent(this, saveUser));

        return saveUser;
    }

    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public void deleteUser(Long id) {
        userRepository.delete(id);
    }
}
기능설명
@ServiceSpring이 관리하는 서비스 계층 컴포넌트
@Autowired필드 기반 의존성 주입 (스프링 5 이후에는 생성자 주입 권장)
register()회원 등록 후 UserRegisteredEvent 이벤트 발행
eventPublisher이벤트를 리스너로 전달하는 이벤트 중개자 역할

이벤트 발행 로직

eventPublisher.publishEvent(new UserRegisteredEvent(this, saveUser));
  • 이 코드가 실행되면, UserRegisteredEvent가 발생하고
  • 이를 감지한 @EventListener 클래스에서 후처리(예: 이메일 전송)를 진행할 수 있습니다.

DI 권장 방식 비교

방식설명권장 여부
@Autowired 필드 주입코드 간결하지만 테스트 어려움❌ (비권장)
생성자 주입 (@RequiredArgsConstructor)불변성 보장 + 테스트 용이✅ (권장)

즉, UserService는 아래처럼 바꾸는 게 더 좋습니다:

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;
    ...
}

repository

UserRepository – 인메모리 회원 저장소

@Repository
public class UserRepository {
    private final List<User> list = new ArrayList<>();

    public UserRepository() {
        list.add(new User(1L, "test1", "1234", "홍길동"));
        list.add(new User(2L, "test2", "1234", "박길동"));
        list.add(new User(3L, "test3", "1234", "최길동"));
    }

    public User save(User user) {
        findById(user.getId()).ifPresent(list::remove);
        list.add(user);
        return user;
    }

    public Optional<User> findById(Long id) {
        return list.stream().filter(x -> x.getId().equals(id)).findFirst();
    }

    public List<User> findAll() {
        return list;
    }

    public long count() {
        return list.size();
    }

    public void delete(Long id) {
        list.removeIf(x -> x.getId().equals(id));
    }

    public boolean existsById(Long id) {
        return list.stream().anyMatch(x -> x.getId().equals(id));
    }
}
  • @Repository Spring에서 Bean으로 등록됨. DAO 계층 의미
  • List<User> list DB 대신 임시로 사용하는 인메모리 리스트
profile
Backend engineer

0개의 댓글