EJB(Enterprise JavaBeans) 알아보기
개요
- 스프링 이전의 기술들을 살펴보면 EJB에 대한 이야기가 많이 나온다.
- 김영한 선생님 마저 눈을 감고 계시는 악명 높은 EJB가 뭔지 궁금했다.
- 이번 기회를 통해 알아보자.
EJB의 역할
- 지난 포스팅(JSP와 서블릿이 무엇일까?)에서 MVC 패턴 중 서블릿은 Controller를, JSP는 View를 맡았다고 했다.
- 그렇다면 model은 누가 맡을까?
- 서블릿(
1997
)과 JSP(1999
)가 등장한 시기 즈음에 등장한 EJB(1998
)가 자연스럽게 Model의 역할을 맡았다.
EJB가 무엇인가?
- EJB는 자바 기반의 서버 측 컴포넌트 아키텍처로, 대규모 엔터프라이즈 애플리케이션에서 비즈니스 로직을 처리한다.
- 대규모 엔터프라이즈 애플리케이션에서는 트랜잭션의 일관성, 데이터 무결성 등이 아주 중요하다.
- EJB는 다음과 같은 장점으로 대규모 시스템에서 좋은 평가를 받았다.
- 트랜잭션을 자동으로 관리하여, 개발자가 직접 처리할 필요가 없다.
- 분산 트랜잭션도 지원하여 여러 시스템 간의 트랜잭션을 쉽게 처리할 수 있다.
- 인증/인가를 내장하여 보안이 뛰어나다.
- 원격 호출을 통해 분산 컴퓨팅을 지원한다.
- 자원 풀링, 캐싱 등으로 성능을 최적화한다.
EJB의 종류
- EJB는 크게 3가지 유형으로 나뉜다.
[1. Session Bean]
- 비즈니스 로직을 처리하는 데 사용된다.
스프링에서 @Service가 붙은 클래스라고 보면 된다.
- Stateless Session Bean
- 클라이언트의 상태를 유지하지 않고, 모든 요청을 독립적으로 처리한다.
1 2 3 4 5 6 7 8 9 10 11 12 13
@Stateless public class CalculatorStatelessBean { // 상태 저장 x public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { return a - b; } }
- Stateful Session Bean
- 클라이언트의 상태 정보를 저장하여, 같은 클라이언트와 계속 상호작용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@Stateful public class ShoppingStatefulBean { private int itemCount = 0; // 상태 저장 public void addItem(int count) { this.itemCount += count; } public void removeItem(int count) { this.itemCount -= count; } public int getItemCount() { return itemCount; } }
- Singleton Session Bean
- 애플리케이션 내에서 단 하나의 인스턴스만 존재하며, 전역적으로 공유된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@Singleton public class CounterSingletonBean { private int count = 0; // 상태 저장 - 전역 상태나 리소스 관리에 사용된다. public void increment() { count++; } public void decrement() { count--; } public int getCount() { return count; } }
[2. Entity Bean]
- 데이터베이스와 상호작용을 하기 위해 사용된다.
스프링에서 @Repository가 붙은 클래스라고 보면 된다.
- EJB 3.0 이후에는 JPA로 대체되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Stateless
public class UserServiceBean {
@PersistenceContext
private EntityManager entityManager;
// 사용자 등록
public void createUser(String name, String email) {
User user = new User(name, email);
entityManager.persist(user);
}
// ID로 사용자 조회
public User findUserById(Long id) {
return entityManager.find(User.class, id);
}
// 모든 사용자 조회
public List<User> findAllUsers() {
return entityManager.createQuery("SELECT u FROM User u", User.class).getResultList();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
InitialContext context = new InitialContext();
UserServiceBean userService
= (UserServiceBean) context.lookup("java:global/myapp/UserServiceBean");
// 새 사용자 등록
userService.createUser("root", "ajroot5685@github.io");
// 사용자 조회
User user = userService.findUserById(1L);
System.out.println("이름 : " + user.getName() + ", 이메일 : " + user.getEmail());
// 모든 사용자 조회
List<User> users = userService.findAllUsers();
for (User u : users) {
System.out.println("id : " + u.getId() + ", 이름 : " + u.getName());
}
[3. Message-Driven Bean]
- 비동기 메세지를 처리하기 위해 사용되며,
Java Message Service(JMS)
와 연동하여 비동기 작업을 처리한다.
Listener
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
@MessageDriven(activationConfig = {
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "javax.jms.Queue"
),
@ActivationConfigProperty(
propertyName = "destination",
propertyValue = "java:/jms/queue/myQueue"
)
})
public class MessageProcessorBean implements MessageListener {
@Override
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
String text = textMessage.getText();
System.out.println("Received message: " + text);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Publisher
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// JMS 리소스 찾기
InitialContext context = new InitialContext();
ConnectionFactory connectionFactory = (ConnectionFactory) context.lookup("java:/ConnectionFactory");
Queue queue = (Queue) context.lookup("java:/jms/queue/myQueue");
// JMS 커넥션 생성
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(queue);
// 메시지 생성 후 큐에 전송
TextMessage message = session.createTextMessage("Hello, JMS World!");
producer.send(message);
System.out.println("메세지 발송: " + message.getText());
producer.close();
session.close();
connection.close();
EJB의 한계
- 학습 난이도가 굉장히 높고, 자원 관리나 설정이 복잡하고 무거워
POJO(Plain Old Java Object)
또는 경량화된 Spring 프레임워크로 넘어가게 되었다. - 주요 단점은 다음과 같다.
클라이언트가 빈에 접근하기 위해서는 해당 빈을 생성하는 인터페이스가 추가로 필요하다.
빈은
SessionBean
인터페이스를 구현해야 하는데, EJB 라이프사이클 메서드를 명시적으로 구현해야 했다.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
public class CalculatorBean implements SessionBean { @Override public void ejbCreate() {} // 빈 생성 시 처리 @Override public int add(int a, int b) { return a + b; } @Override public int subtract(int a, int b) { return a - b; } @Override public void setSessionContext(SessionContext context) {} // 세션 컨텍스트 설정 @Override public void ejbRemove() {} // 빈 제거 처리 @Override public void ejbActivate() {} // 활성화 처리 @Override public void ejbPassivate() {} // 비활성화 처리 }
XML 설정이 복잡했다. 이는 초기 스프링도 마찬가지로 넘어가겠다.
JNDI를 통한 접근
- EJB 2.x에서는 클라이언트가 빈에 접근하기 위해 JNDI를 사용해야 했는데 매번 처리하는 것이 번거롭고 코드가 지저분해졌다.
1 2 3 4 5 6 7 8 9 10 11
public class EJBClient { public static void main(String[] args) throws Exception { Context context = new InitialContext(); CalculatorLocalHome home = (CalculatorLocalHome) context.lookup("java:comp/env/ejb/CalculatorBean"); CalculatorLocal calculator = home.create(); int sum = calculator.add(10, 5); System.out.println("합계 : " + sum); } }
분산 처리 vs MSA(Micro Service Architecture)
- 요즘 대규모 시스템 하면
MSA
로 구성된 시스템을 떠올리기 쉽다. - 그럼 EJB가 대규모 시스템에 적합하다고 했는데, MSA에 적합할까? 결론부터 말하자면 전혀 다르다.
요소 | EJB(분산 컴퓨팅) | MSA |
---|---|---|
아키텍처 구조 | 모놀리식 구조에서 비즈니스 로직의 중앙 집중화 | 독립된 마이크로서비스들의 느슨한 결합 구조 |
통신 방식 | RMI, JNDI 기반의 동기적 통신 | REST, gRPC, 메세지 큐 등의 다양한 방식의 통신 |
유연성 및 독립성 | 중앙화된 구조로 독립성 제한 | 각 서비스의 독립성과 자율성 보장 |
- MSA에서는 독립적인 컴포넌트들이 상호작용하는 것이므로, 하나의 컴포넌트에 트래픽이 몰리면 해당 컴포넌트만 확장시켜주면 된다.
- 그러나 EJB는 중앙 집중식 구조이기 때문에 모든 요청이 몰리므로 부하가 발생한다.
[Remote Method Invocation]
- 원격 시스템에서 실행 중인 자바 객체의 메서드를 로컬에서 호출하듯 호출하게 해준다.
- 클라이언트는 위에서 JNDI를 통해 로컬 빈 객체를 접근하듯이 호출하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RMIServer {
public static void main(String[] args) {
try {
// 원격 객체 생성
Calculator calculator = new CalculatorImpl();
// RMI Registry 생성 및 원격 객체 바인딩
Registry registry = LocateRegistry.createRegistry(1099); // 기본 포트: 1099
registry.bind("CalculatorService", calculator);
System.out.println("Calculator RMI Server is running...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RMIClient {
public static void main(String[] args) {
try {
// RMI Registry에서 원격 객체 찾기
// localhost 자리에는 다른 서버 IP나 도메인 주소가 들어갈 수 있다.
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
Calculator calculator = (Calculator) registry.lookup("CalculatorService");
// 원격 메서드 호출 - 로컬 메서드를 호출하는 것과 형식이 같다.
int sum = calculator.add(5, 3);
System.out.println("Sum: " + sum);
int diff = calculator.subtract(10, 4);
System.out.println("Difference: " + diff);
} catch (Exception e) {
e.printStackTrace();
}
}
}
[그림으로 알아보기]
- RMI, JNDI와 같은 기술을 통해 원격 메서드를 쉽게 사용할 수 있다.
- 이는 중앙 서버에서 실제 비즈니스 작업을 다른 원격 서버에 위임시킬 수 있다는 뜻이 되고 결과적으로 중앙 서버의 부하가 줄어드는 효과를 볼 수 있다.
- 즉, 모든 요청은 중앙 서버를 통과하지만 실제 비즈니스 작업은 다른 서버가 수행하게 된다.
- 반면 MSA에서는 요청이 하나의 서버에 집중되지 않고, 독립된 여러 서버로 나뉘어진다.
- 때문에 EJB의 분산 처리와 MSA는 구조적으로 다르다.
This post is licensed under CC BY 4.0 by the author.