JNDI: JBoss NS는 어떻게 동작 할 까요?

Choog Yul Lee·2023년 1월 29일
0
post-thumbnail

🥗 Prologue

시스템을 재시작하고 클라이언트 연결이 정상적으로 되지 않는 문제가 발생했다. 관련자들이 모여 재현 테스트를 진행하는데 시작부터 기분이 좋지 않다. 문제를 해결하려고 하는 사람이 보이지 않는다. 모두 자기 문제가 아니라고 한다. 그중 한 명은 테스트가 종료가 되지도 않았는데 로그하나 던져주고는 이렇게 말한다.

"이제 어떻게 정리하실 거에요?"

근데 다들 저 로그가 뭘 뜻하는지 알고나 저렇게 말하는 건지! 애들 대하 듯 하나하나 로그를 읽어 주었다. 그랬더니 이렇게 말한다.

"여기는 그런거 볼 사람 없어요!"

아 진짜!!
모르는 건 자랑이 아닌데!
계속 모른다고!
몰라서 못 한다고만 한다. 👊

🎪 System Configuration Diagram

문제를 해결하기 위해서는 주변을 먼저 파악하는 것이 중요하다. 먼저, 시스템이 어떤구성으로 이루어져 있는지 확인한다. 아래는 내가 운영하는 시스템을 도식화한 그림이다.

간단하게 설명하면,

  • 고객은 JEUS Application Server(AS)에서
  • 내가 배포한 my-api.jar 를 이용하여
  • 내가 운영하는 JBoss AS와 연결한다.
  • 이 기종 WAS 를 연결하는 기술로는 JNDI 와 RMI 가 사용된다.

🔑 Problem

위 구성에서 JBoss AS를 재시작 하니 JEUS AS 와 연결을 맺지 못 한다. 문제를 일으키는 부분은 InitialContext 를 생성 하는 부분으로 아래와 같다. org.jnp.interfaces.NamingContextFactory 의 진입점을 JEUS에서 못 찾는 듯하다.

Problem Code

Properties properties = new Properties();
properties.put(Context.SECURITY_PRINCIPAL, "guest");
properties.put(Context.SECURITY_CREDENTIALS, "guest");
properties.put(Context.PROVIDER_URL, "IP:PORT");
properties.put(Context.INITIAL_CONTEXT_FACTORY, 
"org.jnp.interfaces.NamingContextFactory");

// the error occur on this line!
Context context = new InitialContext(properties);

Error Log
Error_Log

🔪 Analysis

로그javax.naming.InitialContext.init 부분을 통해 에러는 JNDI 관련 부분에서 발생하는 것을 확인 할 수 있다. 그럼 JNDI 를 시작점으로 차근차근 분석 해볼까!

🔍 What is JNDI?

JNDIJ2EE 서비스 중 하나로 The Java Naming and Directory Interface 의 줄임말이다. 왠지 영어가 기니까 어렵게 느껴지는데. 단지 용어가 낯선 것일 뿐이지 쉽다. Naming ServiceDirectory Service 만 이해하면 된다. 글을 읽는데 필요한 정도로만 이해해 보자.

🚩 Naming Service
이름을 이용해 컴퓨터 자원을 접근하도록 도와 주는 서비스

🚩 Directory Service
디렉터리 안에 속성(attribute) 대해서 생성, 추가, 조회, 수정, 조회 할 수 있도록 인터페이스를 제공하는 것

낯선 이름 이지만 우리는 해당 서비스를 이미 사용해 왔다. 단지 너무 자연스러워 사용하는 것 조차 몰랐을 뿐!

우리가 run.sh 라는 shell script 를 사용하여 프로그램을 실행 시킬 수 있는 건, File Systemrun.sh 에 해당하는 reference 를 이미 연결시켰기 때문에 가능한 일이다. File System 은 우리가 가장 많이 사용하는 Naming Service 로 생각할 수 있다.

run.sh

디렉터리 안에 어떤 자원을 찾기 위해 때로는 find 명령을 사용하는 데, 이는 File System이 해당 디렉터리안에 속성을 모두 관리하기 때문에 사용 가능 하다. 그럼 File SystemDirectory Service 로도 생각할 수 있다.

# 현재 위치에서 log가 들어가는 파일 모두 찾기
find . -name "*log*"

JNDI는 앞에 Java가 붙었으니까 이렇게 이해하면 된다.

🚩 JNDI 는,
Java Language에서 객체를 이름으로 접근할 수 있도록 도와주는 인터페이스이다.

🔍 JNDI Architecture

앞서 JNDI 가 무엇을 하는 것인지 알았음으로 JNDI를 좀더 들여다 보자. 그림에서 보는 것처럼 JNDIAPISPI 로 구성된다.

🚩 Application Programming Iterface(API) 는 개발자가 사용한다.
🚩 Service Provider Interface(SPI) 는 Vendor 가 구현하여 제공한다.

이렇게 두개의 Layer를 두는 이유는 Vendor 의 종속성을 제거하기 위해서 인데, 이를 통해 Application의 변경을 최소화 할 수 있다.

이상적으로, (아주 이상적으로)
Application이 운영되는 환경이 Jboss 에서 WebSphere 로 변경된다 하여도 우리는 Application 을 변경할 필요가 없어진다. 단지 참조하는 jarWebSphere 에서 제공하는 jar 로 교체하기만 하면(SPI Layer만 교체하면) 될 뿐이다. 그럼 Application은 WebSphere 환경에서 정상적으로 동작한다.

아래 그림은 JNDI 구조를 내가 운영하는 환경에 맞게 투형시킨 것이다.

JESU에서 JBoss JNDI Service에 접속하기 위해 아래의 절차로 동작하는데, 로그를 통해 유추해 보면 문제는 SPI 구현체를 찾는 부분 2번에서 발생하는 듯 보인다.

  1. Application은 JNDI Format를 사용하여 연결을 요청한다.
  2. JNDI APINamging Manager를 이용하여 SPI 구현체를 찾는다.
  3. 그리고 SPI 구현체를 초기화 한다.
  4. JNDI SPI는 요청을 SPI 구현체, 즉 Jboss Naming Service에 보낸다.
  5. Jboss Naming Service 는 요청을 처리하여 응답을 반환한다.
  6. JNDI SPI는 응답을 JNDI Format 으로 변경한다.
  7. Namging Manager는 구현체 이름을 등록한다.
  8. JNDI API 는 응답을 Application 에 보낸다.

Application 은 내가 배포한 my-api.jar 를 사용하여 JNDI 연결을 시도한다.

🔍 Using JNDI API

Java 에서는 실제로 Namaing Service에 접근하기 위한 클래스를 제공하는 데 그중 가장 중요한 클래스는 ContextInitialContext이다.

🚩 javax.naming.Context

  • JNDI 명세에 기반한 Interface 이다.
  • Object 를 생성, 추가, 조회, 수정, 조회 가능한 Method를 제공한다.
  • 이 인터페이스는 Vendor에 의해서 구현되어야 한다.

🚩 javax.naming.InitialContext

  • javax.naming.Context 의 구현체이다.
  • 객체 이름을 확인하기 위한 시작점을 제공한다.
  • Naming Service의 환경 정보는 InitialContext 생성자 파라미터로 전달된다.
  • InitialContext를 생성할 때 Context를 바로 생성 할지 아니면 사용 전까지 대기 할지는 Vendor 의 구현을 따른다.

    "젠장, 문제의 원인을 밝히기 위해서는 JBoss의 Naming Service가 어떻게 동작하지는도 확인해 봐야 할 것 같다. 💧"

javax.naming.InitialContext 생성을 위서는 Servier Provider 정보가 필요한다. 속성값으로 전달한다.

첫째, INITIAL_CONTEXT_FACTORY 설정

  • KEY: Context.INITIAL_CONTEXT_FACTORY
  • VALUE: org.jnp.interfaces.NamingContextFactory

org.jnp.interfaces.NamingContextFactoryjavax.naming.spi.InitialContextFactory의 구현체로 JBoss Naming Service를 사용할 수 있게 해준다.

둘째, PROVIDER_URL 설정

  • KEY: Context.PROVIDER_URL
  • VALUE: IP:PORT

IP:PORT는 해당 IP와 PORT로 Naming Service 제공하는 서버를 가르킨다.

전체 코드는 🔑 Problem 파트에서 보여준 Problem Code 와 동일하다. jboss-as-all-client.jar가 ClassPath에 있다면 해당 코드는 문제 없이 컴파일 되는 것을 확인 할 수 있다.
JNDI Connect Code

public class App {
    public static void main(String[] args) {
        try {
			Properties properties = new Properties();
			properties.put(Context.SECURITY_PRINCIPAL, "guest");
			properties.put(Context.SECURITY_CREDENTIALS, "guest");
			properties.put(Context.PROVIDER_URL, "IP:PORT");
			properties.put(Context.INITIAL_CONTEXT_FACTORY, 
			"org.jnp.interfaces.NamingContextFactory");

			// the error occur on this line!
			Context context = new InitialContext(properties);
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}

그럼 동일한 코드인데 왜 에러가 발생하고 Reconnect이 되지 않았을까? 우리는 그 단서를 로그에서 찾을 수 있다.

at jeus.service.archive.ArchiveArrayClassLoader.getResources(ArchiveArrayClassLoader.java:256)
at com.sun.naming.internal.ResourceManager.getApplicationResources(ResourceManager.java:571)
at com.sun.naming.internal.ResourceManager.getInitialEnvironment(ResourceManager.java:256)
at javax.naming.InitialContext.init(InitialContext.java:251)
at javax.naming.InitialContext.<init>(InitialContext.java:227)

InitialContext 에서 Context를 생성하기 위해서 뭔가를 찾는거 같은데 뭘 찾는 걸까?
🚩 javax.naming.InitialContext에서 이야기했지만 Context를 생성하는 건 Vendor 의존적이다. Reconnect 할때 InitialContext 에서 발생하는 에러를 이해하기 위해서는 JBoss Naming Service가 동작하는지 먼저 이해해야 한다.

🔍 Understanding the JBossNS

JBossNS 는 Java socket과 RMI 기반의 javax.naming.Context 인터페이스의 구현체이다. Client와 Server 구조로 되어있어 서로 다른 JVM에서 서비스를 연결하는 것에 최적화 되어 있다. 같은 JVM 을 사용할 경우 socket이 사용하지 않으며, 그대신 global singleton 처럼 객체에 접근을 가능하도록 설계되어 있다.

아래 JBossNS 를 구성하는 주요 클래스 구성도만 보아도 앞서 이야기한 JNDI Connect Code 가 어떻게 동작할지 대략적으로 짐작할 수 있을 것 같다.

하지만 왜 에러가 발생하는지 원인을 알기 위해서는 javax.naming.InitialContext가 어떻게 javax.naming.Context를 생성하는지 상세히 알아야 한다.

Client에서 JBossNS 속성과 함께 InitialContext 를 생성할 때, Context 를 생성하기 위해서 org.jnp.interfaces.NamingContextFactory 가 사용된다. NamingContextFactoryjavax.naming.spi.InitialContextFactory 인터페이스의 JBossNS 구현체 이다. NamingContextFactoryContext 를 생성하라고 요청받으면, InitialContext 의 생성자 파라미터의 환경 정보와 전역 JNDI Namespace를 이용하여 org.jnp.interfaces.NamingContext를 생성한다.

JNDI determines each property's value by merging the values from the following two sources, in order:
1. The first occurrence of the property from the constructor's environment parameter and (for appropriate properties) the applet parameters and system properties.
2. The application resource files (jndi.properties).
출처 : javadoc-InitialContext

NamingContext는 실제 Context의 구현체이고, JBossNS 서버에 연결하는 작업을 수행한다. Context.PROVIDER_URLNamingServer 의 RMI Reference 를 가르키고 있다.

NamingContext 인스턴스와 NamingServer 의 연결은 Context의 첫 번째 동작에서 이루어진다. 즉, InitialContext를 생성할 때 Context를 생성하지 않는다 뜻이다. Lazy Fashion하게 동작한다. Context 에서 어떤 작업을 수행할 때, NamingContextNamingServer와 연결을 확인한다. 연결이 없다면 NamingContextContext.PROVIDER_URL 값을 확인한다. PROVIDER_URL 값이 존재 한다면, NamingContextNaming 인스턴스를 찾기위해 NamingContext class static map에서 host와 port 값으로 이루어진 Key를 찾는다. Key(Host와 Port 의 쌍으로 이루어진 값)가 존재하면 Naming 인스턴스가 이미 생성되어 JVM상에 존재하고 있으므로 이미 생성된 Naming 인스턴스를 사용한다. Key가 없으면, NamingContextjava.net.Socket을 사용하여 Naming RMI stub을 JBossNS Server로 부터 얻어온다. NamingContext는 새로이 얻어온 Naming 객체를 NamingContext server map 저장한다.

요약하면,
1. org.jnp.interfaces.NamingContextFactoryorg.jnp.interfaces.NamingContext 생성
2. NamingContext 는 이미 JBossNS Server연결 하여 Naming(JBossNS에서의 Context) 객체가 JVM에 있는지 확인
3. 없고 처음이면 JBossNS Server 부터 Naming를 받아 옴
4. 받아온 Naming 객체를 Chache에 저장

Context.PROVIDER_URL 값이 존재 하지 않는 경우는 단순하게 Main MBean 에서 값을 가져온다.

Context 의 구현체인 NamingContext는 모든 동작을 Naming 인스턴스에 위임한다. 대신, NamingContext 는 전달받은 JNDI Name을 JBossNS server 가 인식할 수 있는 이름으로 변경한다. 왜냐면 JNDI Name 은 매우 상대적이거나 URL 일수 있기 때문이다. 이런 기능을 제공함으로써 NamingContextNaming 의 Context 역할을 수행한다.

정상적인 상황에서 Heap Dump 와 Thread Dump를 확인해 보면 필요한 클래스가 모두 있다는 걸 확인 할 수 있다.

3CLTEXTCLASS   			org/jnp/interfaces/NamingContextFactory(0x0000000031587200)

3CLTEXTCLASS   			org/jnp/interfaces/NamingContext(0x0000000031589D00)

3CLTEXTCLASS   			org/jnp/server/NamingServer_Stub(0x0000000031595600)

3CLTEXTCLASS   			org/jnp/interfaces/Naming(0x0000000031594F00)

문제의 발생은 org/jnp/interfaces/Naming 각 Heap 존재 하지 않아 발생하는 것이 아닐까? 유추해 볼수 있다.

4월에 있을 다음 재현 테스트 내 가정이 맞을지 틀릴지 확인 할 수 있겠지!

🍰 Epilogue

글로 정리하는게 너무 어려웠다. 솔직히 너무 어려워 이 글을 누가 볼까 싶다. 이 글을 완성하고 나서 더러운 기분이 모두 사라졌다. 내가 세운 논리가 완벽하고 조화롭다. 여기까지 스스로 생각하고 이루어낸 내가 너무도 대견하다. 👏

나는 돈을 은행에 보관하고 싶다.
은행 계좌가 없으므로 은행가서 계좌를 만들었다.
그 은행계좌를 종이에 적어 금고에 넣어 둔다.
은행에 송금하려고 하는데 송금이 되지 않는다.
난 계좌가 적힌 종이를 확인하려 금고를 열었다.
어! 종이는 있는데 계좌가 지워져 있네.
계좌번호를 알기위해 난 어떻게 해야 할까?

🛰️ Reference Site

1개의 댓글

comment-user-thumbnail
2023년 1월 30일

기본적인 JNDI 구조, 개념과 Namaing Service와의 연계등 쉽게 접근하기 어려운 내용들을 에러와 예제를 통해 쉽게 이해 할 수 있었습니다.
특히 마지막의 은행에 비유한 글은 인상 깊었습니다.

답글 달기