Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file.
38 changes: 38 additions & 0 deletions topics/06-event/members/sujin/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 6주차 학습용 — Spring Event (publishEvent / @EventListener / @TransactionalEventListener / @Async)
// 도메인: #2 결제(payment). STAGE 1 은 jdbc 없어도 되지만, STAGE 2 트랜잭션 결합부터 H2 사용.

plugins {
id 'java'
id 'application'
}

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

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.2.0'

// STAGE 2 트랜잭션 결합 — H2 인메모리
implementation 'org.springframework.boot:spring-boot-starter-jdbc:3.2.0'
runtimeOnly 'com.h2database:h2:2.2.224'

// STAGE 4 비교용 — 5 주차 @Audited(AOP) vs 6 주차 이벤트 결과 비교
implementation 'org.springframework.boot:spring-boot-starter-aop:3.2.0'
}

application {
// 실행 시 -PmainClass=stage.s1.Stage1HelloEvent 로 덮어씀
mainClass = providers.gradleProperty('mainClass').orElse('stage.s1.Stage1HelloEvent')
}

// -parameters: @EventListener(condition = "...") SpEL / 파라미터 바인딩 안전장치 (5 주차에서 익힘)
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << '-parameters'
}
240 changes: 240 additions & 0 deletions topics/06-event/members/sujin/measurements.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package infra;

import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* 측정 / 관찰 결과를 본인 폴더의 measurements.md 에 자동 누적 기록.
* (1~3 주차 example 과 동일한 자동 위치 감지 패턴)
*
* <h3>자동 위치 감지</h3>
* IntelliJ Working Directory 설정과 무관하게, 이 클래스의 실제 위치에서
* 위로 올라가며 build.gradle 가진 폴더를 찾아 그 안에 measurements.md 를 만든다.
*
* <h3>6 주차(Spring Event) 형식</h3>
* 동시성 측정(누락/실패/ms)이 아니라 자유 텍스트 관찰이 많으므로 {@code save(stage, note)} 사용.
* <pre>
* MeasurementLog.save("s1", "HelloEvent 발행 → 동기 리스너 호출 순서 확인 (thread=main)");
* // → measurements.md 에 "- [06-08 14:00] s1 · HelloEvent 발행 → ..." 한 줄 append
* </pre>
* 시간 측정이 필요한 STAGE 3(@Async) 에선 {@code save(stage, note, millis)} 로 ms 도 기록.
*/
public final class MeasurementLog {

private static final Path FILE = resolveFile();
private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("MM-dd HH:mm");

private MeasurementLog() {}

/** 자유 텍스트 관찰 (STAGE 1~2 주로 사용). */
public static void save(String stage, String note) {
String line = String.format("- [%s] %s · %s%n",
LocalDateTime.now().format(TIME), stage, note);
append(line);
}

/** 시간 측정 포함 (STAGE 3 @Async — publisher 블록 시간 등). */
public static void save(String stage, String note, double millis) {
String line = String.format("- [%s] %s · %s (%.1fms)%n",
LocalDateTime.now().format(TIME), stage, note, millis);
append(line);
}

private static void append(String line) {
try {
if (!Files.exists(FILE)) {
Files.writeString(FILE,
"# 측정 기록 (6주차 — Spring Event)\n\n자동 누적. 옆에 해석 메모는 직접 추가하세요.\n\n");
}
Files.writeString(FILE, line, StandardOpenOption.APPEND);
System.out.println("→ " + FILE.toAbsolutePath() + " 에 기록됨");
} catch (IOException e) {
System.err.println("⚠️ measurements.md 기록 실패: " + e.getMessage());
}
}

private static Path resolveFile() {
try {
var domain = MeasurementLog.class.getProtectionDomain();
if (domain == null) return Path.of("measurements.md");
var codeSource = domain.getCodeSource();
if (codeSource == null) return Path.of("measurements.md");
URL location = codeSource.getLocation();
if (location == null) return Path.of("measurements.md");

Path start = Path.of(location.toURI());
if (Files.isRegularFile(start)) {
start = start.getParent();
}

Path current = start;
for (int i = 0; i < 10 && current != null; i++) {
if (Files.exists(current.resolve("build.gradle"))
|| Files.exists(current.resolve("build.gradle.kts"))) {
return current.resolve("measurements.md");
}
current = current.getParent();
}
} catch (Exception ignored) {}
return Path.of("measurements.md");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package stage.s1;

import infra.MeasurementLog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

// (1) 이벤트 — record 로 충분 (Spring 4.2+). 완성본.
record HelloEvent(String message) {}

// (2) Publisher — ApplicationEventPublisher 주입
@Service
class HelloService {
private final ApplicationEventPublisher publisher;

HelloService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

void sayHello(String name) {
System.out.println("[publisher] sayHello — " + name
+ " / thread=" + Thread.currentThread().getName());

publisher.publishEvent(new HelloEvent("hello " + name));

System.out.println("[publisher] return"
+ " / thread=" + Thread.currentThread().getName());
}
}

// (3) Listener — @EventListener 메서드 1 개
@Component
class HelloListener {

@EventListener
void onHello(HelloEvent event) {
String message = event.message();

System.out.println("[listener] received — " + message + " / threads=" + Thread.currentThread().getName());
}
}

// (4) main
@SpringBootApplication
public class Stage1HelloEvent {
public static void main(String[] args) {
var ctx = SpringApplication.run(Stage1HelloEvent.class, args);
ctx.getBean(HelloService.class).sayHello("world");

MeasurementLog.save("s1-1", "HelloEvent 발행 → 동기 리스너 호출 순서 println 확인");

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package stage.s1;

import infra.MeasurementLog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

// 1-2 전용 이벤트 — 1-1 의 HelloEvent 와 분리해서 리스너 교차 발화 방지.
// (payload 타입이 곧 라우팅 키 — 1-5 에서 다시 나올 개념)
record OrderDemoEvent(String message) {}

@Service
class OrderDemoPublisher {
private final ApplicationEventPublisher publisher;

OrderDemoPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

void publish() {
System.out.println("[publisher] publish");
publisher.publishEvent(new OrderDemoEvent("hi"));
System.out.println("[publisher] return");
}
}

@Component
class ListenerA {
@EventListener
@Order(2)
void on(OrderDemoEvent e) {
System.out.println("[A] " + e.message());
}
}

@Component
class ListenerB {
@EventListener
@Order(1)
void on(OrderDemoEvent e) {
System.out.println("[B] " + e.message());
}
}

@Component
class ListenerC {
@EventListener
@Order(0)
void on(OrderDemoEvent e) {
System.out.println("[C] " + e.message());
}
}

@SpringBootApplication
public class Stage1_2_Order {
public static void main(String[] args) {
var ctx = SpringApplication.run(Stage1_2_Order.class, args);
ctx.getBean(OrderDemoPublisher.class).publish();

MeasurementLog.save("s1-2", "@Order로 리스너 호출 순서 제어 확인 (작은 값 먼저)");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package stage.s1;

import infra.MeasurementLog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

record ExceptionDemoEvent(String message) {}

@Service
class ExceptionDemoPublisher {
private final ApplicationEventPublisher publisher;

ExceptionDemoPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

void publish() {
System.out.println("[publisher] publish");
publisher.publishEvent(new ExceptionDemoEvent("hi"));
System.out.println("[publisher] return"); // ← 이 줄이 찍히는지 관찰 포인트
}
}

@Component
class FirstListener {
@EventListener
@Order(1)
void on(ExceptionDemoEvent e) {
System.out.println("[L1 order=1] received");
}
}

@Component
class FailingListener {
@EventListener
@Order(2)
void on(ExceptionDemoEvent e) {
System.out.println("[L2 order=2] received — throwing");

throw new RuntimeException("일부러 실패");
}
}

@Component
class ThirdListener {
@EventListener
@Order(3)
void on(ExceptionDemoEvent e) {
System.out.println("[L3 order=3] received"); // ← 이게 찍히나? 관찰 포인트
}
}

@SpringBootApplication
public class Stage1_3_Exception {
public static void main(String[] args) {
var ctx = SpringApplication.run(Stage1_3_Exception.class, args);

try {
ctx.getBean(ExceptionDemoPublisher.class).publish();
} catch (Exception e) {
System.out.println("[main] caught — " + e.getMessage()); // ← 전파됐나? 관찰 포인트
}

MeasurementLog.save("s1-3", "동기 기준동작: 예외 전파 O, 체인 중단 O — STAGE3 @Async 면 전파 X 예정");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package stage.s1;

import infra.MeasurementLog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

class LegacyDemoEvent extends ApplicationEvent {
private final String message;

LegacyDemoEvent(Object source, String message) {
super(source); // ApplicationEvent는 source(발행 주체) 필수
this.message = message;
}

String message() { return message; }
}

@Service
class LegacyDemoPublisher {
private final ApplicationEventPublisher publisher;

LegacyDemoPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}

void publish() {
System.out.println("[publisher] publish");
publisher.publishEvent(new LegacyDemoEvent(this, "hi"));
System.out.println("[publisher] return");
}
}

// (A) 옛 방식 — ApplicationListener<E> 인터페이스 직접 구현
@Component
class OldStyleListener implements ApplicationListener<LegacyDemoEvent> {
@Override
public void onApplicationEvent(LegacyDemoEvent event) {
System.out.println("[OldStyle] " + event.message());
}
}

// (B) 비교용 — 같은 이벤트를 @EventListener 로 받기 (완성 제공)
@Component
class AnnotationStyleListener {
@EventListener
void on(LegacyDemoEvent e) {
System.out.println("[Annotation] " + e.message());
}
}

@SpringBootApplication
public class Stage1_4_OldStyle {
public static void main(String[] args) {
var ctx = SpringApplication.run(Stage1_4_OldStyle.class, args);
ctx.getBean(LegacyDemoPublisher.class).publish();

MeasurementLog.save("s1-4", "ApplicationListener<E> vs @EventListener — 동작 동일(같은 메커니즘), 단\n" +
" 인터페이스는 클래스당 이벤트 1개 고정");
}
}
Loading