ํฐ์คํ ๋ฆฌ ๋ทฐ
JAVA/SpringBoot
Spring Boot - Server Sent Event๋ฅผ ์ฌ์ฉํ ์ค์๊ฐ ์๋ฆผ
realizers 2022. 10. 28. 23:24728x90
๋ฐ์ํ
๐ก Server Sent Event๋?
- HTTP ์คํธ๋ฆฌ๋ฐ์ ํตํด ์๋ฒ์์ ํด๋ผ์ด์ธํธ์๊ฒ ๋จ๋ฐฉํฅ์ผ๋ก ์๋ฆผ์ ์ ์กํ ์ ์๋ HTML5 ํ์ค ๊ธฐ์ ์ ๋๋ค.
- EventStream์ ์ต๋ ๊ฐ์๋ HTTP/1.1 ์ฌ์ฉ์ 6๊ฐ, ๊ฐ๋จํ ๋งํด ํฌ๋กฌํญ์ 6๊ฐ๊น์ง ์ด์ฉ๊ฐ๋ฅํ๋ฉฐ HTTP/2 ์ฌ์ฉ์ ์ต๋ 100๊ฐ๊น์ง ์ ์งํ ์ ์๋ค๊ณ ํฉ๋๋ค.
- JavaScript์ EventSource๋ฅผ ์ฌ์ฉํ์ฌ ์ปค๋ฅ์ ์ ๋งบ์ ์ ์์ผ๋ฉฐ, ์ ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์๋์ผ๋ก ์ฌ์๋๋ฅผ ํ๋ ํน์ง์ ๊ฐ์ง๊ณ ์์ต๋๋ค.
- IE์์๋ EventSource๋ฅผ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๊ณ ์์ง ์์ง๋ง polyfill์ด๋ผ๋ ๊ฒ์ ์ฌ์ฉํ์ฌ ๊ตฌํํ ์ ์์ง๋ง ์ด์ IE๋ ๋ ๋ฌ์ผ๋ฉฐ ๊ฑฑ์ ํ์ง ์์๋ ๊ด์ฐฎ์ง ์์๊น? ํฉ๋๋ค.
- ํด๋ผ์ด์ธํธ์์ ํ์ด์ง๋ฅผ ๋ซ์๋ ์๋ฒ๊ฐ ์ด๋ฅผ ์ฒดํนํ๊ธฐ ์ด๋ ต์ต๋๋ค. ์์ผ์ ์ฌ์ฉํ๋ฉด disconnect ์ด๋ฒคํธ๋ฅผ ์ ์ ์์ง๋ง SSE๋ ์ด๋ ต์ต๋๋ค.
๐ก ๊ตฌํํ๊ณ ์ ํ๋ ์ ์ฒด์ ์ธ ๊ทธ๋ฆผ
- ์ํฉ์ ๊ฐ๋จํฉ๋๋ค. ์ฌ์ฉ์ A๊ฐ ๊ธ์ ๋ฑ๋กํ์ ๊ฒฝ์ฐ ์ฌ์ฉ์ B ๋๋ SseEmitter์ ๋ฑ๋ก๋ ์ ์ฒด ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ๋ณด๋ด๋ ๊ฒ์ ๋๋ค.
๐ก Spring boot Source
์ปจํธ๋กค๋ฌ
- ํด๋ผ์ด์ธํธ์์ EventSource๋ฅผ ๋ณด๋ผ ๋ ์์ฒญ์ ๋ฐ์๋ค์ด๊ธฐ ์ํ ์ปจํธ๋กค๋ฌ๊ฐ ํ์ํฉ๋๋ค.
- sse ํต์ ์ ํ๊ธฐ์ํด์๋ MIME ํ์ ์ text/event-stream์ผ๋ก ์ค์ ์ ํด์ผํฉ๋๋ค.
- Last-Event-ID๋ sse์ ์ปค๋ฅ์ ๋ง๋ฃ ์๊ฐ ๋ฐ ๋คํธ์ํฌ ์ค๋ฅ๋ก ์ธํด ์ฐ๊ฒฐ์ด ๋์ด์ก์ ๊ฒฝ์ฐ ํด๋ผ์ด์ธํธ๋ ๊ทธ ์๊ฐ๋์ ๋ฐ์ดํฐ๊ฐ ์ ์ค๋ ์ ์์ต๋๋ค. ๊ทธ๋ก์ธํด ํด๋ผ์ด์ธํธ๊ฐ ๋ฐ์ ๋ง์ง๋ง ์์ด๋๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ทธ๋์์ ์ ์ค๋ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ณด๋ผ ์ ์์ต๋๋ค.
@RestController
@RequestMapping("/notification")
@RequiredArgsConstructor
public class NotificationApi {
private final NotificationService notificationService;
@CrossOrigin("*")
@GetMapping(value = "/subscribe", produces = "text/event-stream")
public SseEmitter subscribe(@RequestParam(name = "userId") Long userId,
@RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId) {
return notificationService.subscribe(userId, lastEventId);
}
}
์๋น์ค
- userId๋ฅผ ์ฌ์ฉํ์ฌ emitterId๋ฅผ ๋ง๋๋๋ฐ ๋ง๋๋ ํ์์ userId + currentTimeMillis ์
๋๋ค. ์ด๋ ๊ฒ ๋ง๋๋ ์ด์ ๋ ์์์ ์ธ๊ธํ
Last-Event-ID์ ๊ด๋ จ์ด ์์ผ๋ฉฐ Last-Event-ID๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ง์ง๋ง์ผ๋ก ๋ฐ์ ์ด๋ฒคํธ์ ์์ด๋์ ๋๋ค. ์ด๋ ๊ฒ ๋๋ ์ด์ ๋ ์๋์์ ์ค๋ช ํ๊ฒ ์๋๋ค. - ๋๋จธ์ง ์ฝ๋๋ ์ฒ์ฒํ ์ดํด๋ณด๋ฉด ์ดํด๊ฐ ๋ ๊ฒ์ ๋๋ค.
// Last-Event-Id๊ฐ ์ค๋ณต์ด ๋๋ฉด ์ด๋ค ๋ฐ์ดํฐ๊ฐ ๋ง์ง๋ง์ผ๋ก ๋ณด๋ธ ๋ฐ์ดํฐ์ธ์ง ํ์ธํ ์ ์์ต๋๋ค.
Last-Event-Id = 10
{10, data1}
{10, data3}
{10, data2}
// Last-Event-Id๋ฅผ ์๊ฐ๊ณผ ํจ๊ป ์์ด ์ค์ ํ๋ฉด ์ด๋ค ๋ฐ์ดํฐ๋ฅผ ๋ณด๋๋์ง ์์๋ฅผ ํ์
ํ ์ ์์ต๋๋ค.
Last-Event-Id = 10_1631593143664
{10_2931593143664, data1}
{10_4031593143664, data3}
{10_1831593143664, data2}
@Getter
@AllArgsConstructor
public class NotificationDTO {
private String text;
}
@Service
@RequiredArgsConstructor
public class NotificationService {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
private final Map<String, NotificationDTO> eventCaches = new ConcurrentHashMap<>();
private static final Long EMITTER_SESSION_TIME = (60 * 60 * 1000) * 3L; // 3์๊ฐ
// ๋ธ๋ผ์ฐ์ ์์ eventsource๋ก ์ฐ๊ฒฐํ ๋๋ง๋ค emitter ๊ฐ์ฒด ์์ฑํ์ฌ ๋ฑ๋ก
public SseEmitter subscribe(Long userId, String lastEventId) {
String emitterId = makeIdByUserId(userId);
SseEmitter emitter = new SseEmitter(EMITTER_SESSION_TIME);
// SseEmitter์ ์๊ฐ์ด๊ณผ ๋ฐ ๋คํธ์ํฌ ์ค๋ฅ๋ฅผ ํฌํจํ ๋น๋๊ธฐ ์์ฒญ์ด ์ ์์ ์ผ๋ก ๋์ํ ์ ์๋ค๋ฉด ์ ์ฅํด๋ SseEmitter ์ญ์
emitter.onCompletion(() -> deleteByEmitterId(emitterId));
emitter.onTimeout(() -> deleteByEmitterId(emitterId));
emitter.onError((e) -> deleteByEmitterId(emitterId));
saveNewEmitter(emitterId, emitter);
// ์ฐ๊ฒฐ ํ ์๋ฌด๋ฐ ์๋ต๊ฐ์ ๋ณด๋ด์ฃผ์ง ์์ผ๋ฉด ์ค๋ฅ ๋ฐ์ํ๋ฏ๋ก ๋๋ฏธ ๋ฐ์ดํฐ ๋ฐํ
// 503 error๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํ ๋๋ฏธ ์ด๋ฒคํธ ์ ์ก
String eventId = makeIdByUserId(userId);
sendNotification(emitter, eventId, new NotificationDTO("๋๋ฏธ ๋ฐ์ดํฐ"));
// ํด๋ผ์ด์ธํธ๊ฐ ๋ฏธ์์ ํ Event๊ฐ ์์ ๊ฒฝ์ฐ ์ ์กํ์ฌ Event ์ ์ค ๋ฐฉ์ง
// ์ฆ ์ฐ๊ฒฐ์ด ๋๊ธฐ๊ณ ์ฌ ์ฐ๊ฒฐ๋์์ ๊ฒฝ์ฐ lastEventId๋ก ๋ง์ง๋ง ์ด๋ฒคํธ ์ฐพ์
if (hasLatestNotification(lastEventId)) {
sendLatestNotification(emitter, lastEventId, emitterId);
}
return emitter;
}
// ํน์ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ฆผ ๋ฐ์ก
public void send(Long userId, NotificationDTO notificationDTO) {
Map<String, SseEmitter> emitters = findEmitterByEmitterId(userId.toString());
emitters.forEach((key, value) -> {
saveEventCache(key, notificationDTO);
sendNotification(value, key, notificationDTO);
});
}
// ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ฆผ ๋ฐ์ก
public void sendBroadcast(NotificationDTO notificationDTO) {
Map<String, SseEmitter> broadcast = findAllEmitterBroadcast();
broadcast.forEach((key, value) -> {
saveEventCache(key, notificationDTO);
sendNotification(value, key, notificationDTO);
});
}
// ๊ธฐ์กด์ ๋ฑ๋ก๋์ด ์๋ emitter ์ญ์ ํ ๋ฑ๋ก
private void saveNewEmitter(String emitterId, SseEmitter emitter) {
deleteAllEmitterByEmitterId(emitterId);
emitters.put(emitterId, emitter);
}
// ์บ์ ์ ์ฅ
private void saveEventCache(String eventId, NotificationDTO event) {
eventCaches.put(eventId, event);
}
private Map<String, SseEmitter> findEmitterByEmitterId(String emitterId) {
return emitters.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(emitterId))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Map<String, NotificationDTO> findAllEventCacheByEmitterId(String emitterId) {
return eventCaches.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(emitterId))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
// ๋ฑ๋ก๋ ๋ชจ๋ ํด๋ผ์ด์ธํธ ๋ฐํ
private Map<String, SseEmitter> findAllEmitterBroadcast() {
return emitters.entrySet().stream()
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
// ๋ฑ๋ก๋ Emitter ์ญ์
private void deleteByEmitterId(String emitterId) {
emitters.remove(emitterId);
}
// ์ ์ ์ ๊ด๋ จ๋ ๋ชจ๋ Emitter ์ญ์
private void deleteAllEmitterByEmitterId(String emitterId) {
emitters.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(emitterId))
.forEach(entry -> emitters.remove(entry.getKey()));
}
// ์ ์ ์ ๊ด๋ จ๋ ์บ์ ์ญ์
private void deleteAllEventCacheUserId(String eventId) {
eventCaches.entrySet().stream()
.filter(entry -> entry.getKey().startsWith(eventId))
.forEach(entry -> eventCaches.remove(entry.getKey()));
}
private void sendLatestNotification(SseEmitter emitter, String lastEventId, String emitterId) {
Map<String, NotificationDTO> eventCaches = findAllEventCacheByEmitterId(emitterId);
eventCaches.entrySet().stream()
.filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
.forEach(entry -> sendNotification(emitter, emitterId, entry.getValue()));
deleteAllEventCacheUserId(lastEventId);
}
private void sendNotification(SseEmitter emitter, String emitterId, NotificationDTO data) {
try {
emitter.send(SseEmitter
.event()
.id(emitterId)
.data(data)
);
} catch (IOException e) {
deleteByEmitterId(emitterId);
}
}
private String makeIdByUserId(Long userId) {
return userId + "_" + System.currentTimeMillis();
}
private boolean hasLatestNotification(String lastEventId) {
return !lastEventId.isEmpty();
}
}
๐ก Client Source
- JavaScript์์๋ EventSource๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฒ์ ์ฐ๊ฒฐ์ ๋งบ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ์ ์์ต๋๋ค.
<script type="text/javaScript">
const userId = 10;
const eventSource = new EventSource(`http://localhost:8080/notification/subscribe?userId=${userId}`);
eventSource.onmessage = function (event) {
console.log(event.data);
};
eventSource.onerror = (e) => {
console.log(e);
}
</script>
๐ ์ค๊ฐ ์ ๋ฆฌ
- ์ฌ๊ธฐ๊น์ง Server-Sent-Event๋ฅผ ์ฌ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ฐ๊ฒฐ์ ๋งบ๊ณ ์๋ฆผ์ ๋ฐ์ ์ ์๋ค!๋ ๊ฒ๊น์ง๋ ์๊ฒ๋์์ต๋๋ค.
์ง๊ธ๋ถํฐ๋ ๊ธ ์์ฑ์ ์๋ฆผ์ ๋ฐ์ ์ ์๋๋กํ๋ ์ํฉ์ ๊ฐ์ ํ๊ฒ ์ต๋๋ค. - ์ฌ๊ธฐ์๋ถํฐ๋ ํด๋ผ์ด์ธํธ ์์ค๋ ๋์ผํ๋ ์๋ฒ ์ฝ๋๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๐ก ๊ธ ์์ฑ ์ ์ด๋ฒคํธ ๋ฐ์ก(์๋ฒ ์ฝ๋)
- ํ์ํ ์์
- ๊ธ ์์ฑ์ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์๋ฆผ์ ๋ณด๋ด์ง ์์ต๋๋ค.
- ๊ธ ์์ฑ์ด COMMIT๋๋ฉด ์๋ฆผ์ ๋ณด๋ ๋๋ค.
- ๋น๋๊ธฐ๋ก ์๋ฆผ์ ๋ฐ์กํฉ๋๋ค.
๊ธ ์์ฑ
- ์๋๋ ๊ธ ์์ฑ ํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
public enum ActionType {
UNICAST,
BROADCAST
}
@Service
@Transactional
@RequiredArgsConstructor
public class PostsService {
private final PostsRepository postsRepository;
private final ApplicationEventPublisher applicationEventPublisher;
public Long create() {
Posts posts = Posts.builder()
.title("์ ๋ชฉ์?")
.contents("๋ด์ฉ์?")
.build();
postsRepository.save(posts);
applicationEventPublisher.publishEvent(ActionType.BROADCAST); // ์ด๋ฒคํธ ๋ฐ์
return posts.getId();
}
}
์ด๋ฒคํธ ๋ฆฌ์ค๋
- ์ฌ์ฉ์๊ฐ ๊ธ์ ์์ฑํ์ ๊ฒฝ์ฐ ์์ธ๊ฐ ๋ฐ์ํ์ง ์๋๋ค๋ฉด ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ํธ์ถ๋ฉ๋๋ค. @TransactionEventListener ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ @Transactional ์ด๋
ธํ
์ด์
์ด ์ ์ธ๋ ํด๋์ค ๋ ๋ฒจ ํน์ ๋ฉ์๋ ๋ ๋ฒจ์์ ์ปค๋ฐ์ด ๋ ํ์ ํด๋น ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค.
์ฌ๊ธฐ์ @EventListener ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๊ฒ ๋๋ค๋ฉด Posts ๋ฑ๋ก ์ ์์ธ๊ฐ ๋ฐ์ํ๋๋ผ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๊ฒ ๋ฉ๋๋ค. - ์ด๋ฒคํธ์ ์ก์ ํ์ ์ ๋ง๊ฒ ํน์ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ์กํ ๊ฑด์ง ๋๋ ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ์กํ ๊ฑด์ง ๋ถ๊ธฐ ์ฒ๋ฆฌํฉ๋๋ค.
- ์๋ฆผ ๋ฐ์ก ์ ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ์๋ฆผ์ ๋ณด๋ด๋ ๊ณผ์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก @Async ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํฉ๋๋ค.
@Component
@RequiredArgsConstructor
public class SseEventListener {
private static final String TEXT = "์๋ก์ด ์๋ฆผ์ด ๋์ฐฉํ์ต๋๋ค.";
private final NotificationService notificationService;
@Async
@TransactionalEventListener
public void eventHandler(ActionType actionType) {
if (actionType == ActionType.UNICAST) {
notificationService.send(15L, new NotificationDTO(TEXT));
}
if (actionType == ActionType.BROADCAST) {
notificationService.sendBroadcast(new NotificationDTO(TEXT));
}
}
}
๋น๋๊ธฐ ์ฒ๋ฆฌ Config
- ์๋ฆผ์ ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ๋ฐ์กํ๊ธฐ ์ํด Async๋ฅผ ์ค์ ํด์ค๋๋ค.
- ๋น๋๊ธฐ์ ๋ํด ์์ธํ ๋ด์ฉ์ ์๋ ๋งํฌ๋ฅผ ์ฒจ๋ถํ๊ฒ ์ต๋๋ค.
- ๋น๋๊ธฐ์ ๋์ ๋ฐฉ์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
- @Async ์ค์ ๋ฐฉ๋ฒ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
private static final int CORE_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 90;
private static final int QUEUE_CAPACITY = 10;
private static final boolean TASKS_TO_COMPLETE_ON_SHUTDOWN = true;
private static final int AWAIT_TERMINATION_SECONDS = 60;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("async-");
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setWaitForTasksToCompleteOnShutdown(TASKS_TO_COMPLETE_ON_SHUTDOWN);
executor.setAwaitTerminationSeconds(AWAIT_TERMINATION_SECONDS);
executor.setRejectedExecutionHandler((r, e) -> {
throw new IllegalStateException("๋์ ์์ฒญ์๊ฐ ๋ง์ต๋๋ค. ์ ์ํ์ ๋ค์ ์์ฒญํด์ฃผ์ธ์.");
});
executor.initialize();
return executor;
}
}
๐ ๊ตฌํ ํ ์๊ฒ๋ Server-Sent-Event์ ์ฌ์ค
- AWS API Gateway๋ฅผ ์ฌ์ฉํ๋ค๋ฉด SSE๋ฅผ ์ ์ฉํ ์ ์์ต๋๋ค. ๊ตฌํ ํ ๊ฐ๋ฐ ์๋ฒ์ ๋ฐฐํฌํ๋ฉด์ ๊ณ์ํ์ฌ 504 ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ฐ
๊ตฌ๊ธ๋ง ๊ฒฐ๊ณผ ์ง์์ ํ์ง ์๋๋ค๊ณ ํ์ฌ rabbitMQ๋ฅผ ์ ์ฉํ์ฌ ๊ฐ๋ฐํ๊ณ ์์ต๋๋ค..
https://github.com/streamdata-serverless/streamdata-io-basic-demo-stockmarket-prices/issues/1 - ๋ค์ํธ์๋ rabbitMQ์ MQTT๋ฅผ ์ฌ์ฉํ์ฌ ์๋ฆผ์ ๋ฐ์กํ๋ ํธ์ผ๋ก ๋์์ค๊ฒ ์ต๋๋ค.!
728x90
๋ฐ์ํ
'JAVA > SpringBoot' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
๊ณต์ง์ฌํญ
์ต๊ทผ์ ์ฌ๋ผ์จ ๊ธ
์ต๊ทผ์ ๋ฌ๋ฆฐ ๋๊ธ
- Total
- Today
- Yesterday
TAG
- ํธ๋์ญ์ ๋ ์์๋ฐ์ค ํจํด ์คํ๋ง ๋ถํธ ์์
- spring boot redisson sorted set
- service based architecture
- space based architecture
- spring boot excel download paging
- spring boot excel download oom
- pipe and filter architecture
- ํธ๋์ญ์ ๋ ์์๋ฐ์ค ํจํด ์คํ๋ง๋ถํธ
- spring boot redisson ๋ถ์ฐ๋ฝ ๊ตฌํ
- spring boot poi excel download
- pipeline architecture
- ์๋ฐ ๋ฐฑ์๋ ๊ฐ๋ฐ์ ์ถ์ฒ ๋์
- transactional outbox pattern spring boot
- ๋๋ค ํํ์
- ๋ ์ด์ด๋ ์ํคํ ์ฒ๋
- spring boot redis ๋๊ธฐ์ด ๊ตฌํ
- redis sorted set์ผ๋ก ๋๊ธฐ์ด ๊ตฌํ
- java userThread์ DaemonThread
- redis ๋๊ธฐ์ด ๊ตฌํ
- @ControllerAdvice
- polling publisher spring boot
- spring boot ์์ ๋ค์ด๋ก๋
- redis sorted set
- java ThreadLocal
- microkernel architecture
- spring boot redisson destributed lock
- transactional outbox pattern
- ๊ณต๊ฐ ๊ธฐ๋ฐ ์ํคํ ์ฒ
- ์๋น์ค ๊ธฐ๋ฐ ์ํคํ ์ฒ
- JDK Dynamic Proxy์ CGLIB์ ์ฐจ์ด
์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
๊ธ ๋ณด๊ดํจ