SOAP 프로토콜 사용하기

2024. 5. 5. 14:52BACKEND

728x90

담당 프로젝트에 개발 사항중 외부와 통신을 해야 하는데 restAPI가 아니라 SOAP 프로토콜로 통신해야 하는 개발 내용이 있었다. 값을 수신하기 위해서는 SOAP 수신을 위한 별도의 웹서비스를 톰캣으로 실행시켜야 하는 부분도 있었는데 송신부분에 대한 사용 예만 공유해 보겠다.

SOAP 방식이 너무 생소하고 XML을 파싱해 원하는 값을 가져오는 개발이 너무 불편했다.

심지어 담당 프로젝트는 SK 계열사에서 개발을 진행해야 해서 VDI 라는 별도의 가상머신 같은 환경에서 개발을 해야 했는데 속도가 느리거나 RAM이 부족하거나 여러 불편사항이 있었지만 보안 문제로 VDI 내에서의 코드나 텍스트를 외부 PC로 복사/붙여넣기 할 수 없다는 점이 더더욱 불편했다. 예를들어 개발중 오류가 발생하면 일부 키워드만 직접 타이핑해서 검색해야 한다. 오류 로그를 복사해도 외부 PC로 붙여넣기 할 수 없다, Chat GPT에 물어보거나 구글에 검색하고 싶어도 VDI에서는 특정 IP 이외에 인터넷 연결이 불가능하다.

SOAP 사용해보기

대략적으로 SOAP 프로토콜을 JAVA에서 사용하기위해서는 2가지 방식이 있다. SOAP 요청을 보내기 위한 자바 객체를 생성해 요청을 보내거나 XML 요청을 String으로 만들어 요청하는 방식이다.

처음에는 자바 객체를 생성해 요청보내는 것이 훨씬 관리에 편하겠다는 생각으로 예제를 찾아 적용해 보았다.

의존성 추가

implementation group: 'javax.xml.soap', name: 'javax.xml.soap-api', version: '1.4.0' // SOAP 통신
implementation 'com.sun.xml.messaging.saaj:saaj-impl:1.5.2' // SAAJ 구현 라이브러리
implementation 'jakarta.xml.soap:jakarta.xml.soap-api:2.0.0' // Jakarta SOAP API
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' // SOAP 결과 xml을 처리하기위한 라이브러리
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:2.3.3' // JAXB API
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.3' // JAXB Runtime

의존성을 추가 하자, 예제에서는 gradle을 사용 한다.

import javax.xml.soap.*;

public class SimpleSoapClient {
    public static void main(String[] args) {
        try {
            // SOAP Connection 생성
            SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
            SOAPConnection soapConnection = soapConnectionFactory.createConnection();

            // Endpoint URL 설정
            String url = "http://example.com/soap/service";

            // SOAP 메시지 생성
            MessageFactory messageFactory = MessageFactory.newInstance();
            SOAPMessage soapMessage = messageFactory.createMessage();
            SOAPPart soapPart = soapMessage.getSOAPPart();

            // SOAP Envelope 설정
            SOAPEnvelope envelope = soapPart.getEnvelope();
            envelope.addNamespaceDeclaration("ex", "http://example.com/soap/service");

            // SOAP Body 설정
            SOAPBody soapBody = envelope.getBody();
            SOAPElement soapBodyElem = soapBody.addChildElement("YourOperation", "ex");
            SOAPElement soapBodyElem1 = soapBodyElem.addChildElement("parameter");
            soapBodyElem1.addTextNode("value");

            // MIME 헤더 설정
            MimeHeaders headers = soapMessage.getMimeHeaders();
            headers.addHeader("SOAPAction", "http://example.com/soap/service/YourOperation");

            soapMessage.saveChanges();

            // SOAP 메시지 송신 및 응답 수신
            SOAPMessage soapResponse = soapConnection.call(soapMessage, url);

            // 응답 출력
            printSOAPResponse(soapResponse);

            soapConnection.close();
        } catch (Exception e) {
            System.err.println("Error occurred while sending SOAP Request to Server");
            e.printStackTrace();
        }
    }

    private static void printSOAPResponse(SOAPMessage soapResponse) throws Exception {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        Source sourceContent = soapResponse.getSOAPPart().getContent();
        System.out.print("\nResponse SOAP Message = ");
        StreamResult result = new StreamResult(System.out);
        transformer.transform(sourceContent, result);
        System.out.println();
    }
}

위 코드처럼 javax.xml.soap 라이브러리를 사용해 SOAP 요청을 위한 자바 객체를 생성하고 응답을 받는 처리를 하려고 했다.
그런데 사용해야 하는 SOAP 엔드포인트에서 제공하는 XML에 요구사항에 맞게 객체를 설정하는 부분이 너무 헷갈렸다.
예를들어 soapAction 부분을 지정해주어야 요청을 보낼때와 response 된 xml을 다시 파싱하려고 할 때 xml에 구조에 맞게 파싱할 객체의 정보를 지정해 주어야 한다.
@XmlRootElement(name = ""), @XmlElement(name = "RESULT_MSG") 등의 설정을 통해 xml의 정보와 맞게 맵핑해주어야 하는데 이런 부분이 만만치가 않았다.

실제 이 글에서는 단순한 예제만을 작성하지만 개발당시에는 2~3일 동안 상당한 삽질을 통해 처리 하였다.

자바 객체로 SOAP 요청을 만들어 처리하는 것이 쉽지 않다는 생각이 들자 빨리 개발을 완료해 다른 회사와 함께 통합테스트를 진행해야 하는 초조한 상황에서 String 으로 xml을 만들고 요청을 보내는 방식으로 변경 하였다.


@Value("${soap.endpoint-url}")
private String soapEndpointUrl;

@Value("${soap.action-url}")
private String soapAction;

@PostMapping("reply")
    public ResponseEntity<HashMap<String, Object>> post(@RequestBody Form form) {
        HashMap<String, Object> result = new HashMap<>();
        String responseXML = "";

        try {
            // SOAP 엔드포인트와 네임스페이스 정의
            String soapXml =
                    "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                            "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
                            "  <soap:Body>" +
                            "    <UPDATE xmlns=\"http://UPDATE_Req\">" +
                            "      <UPDATE_Req>" +
                            "        <COUNSEL_ID xmlns=\"\">" + form.getCounselId() + "</COUNSEL_ID>" +
                            "        <STATUS xmlns=\"\">" + form.getStatus() + "</STATUS>" +
                            "        <MODIFIER xmlns=\"\">" + form.getReplyRegName() + "</MODIFIER>" +
                            "      </UPDATE_Req>" +
                            "    </UPDATE>" +
                            "  </soap:Body>" +
                            "</soap:Envelope>";

            // URL 객체 생성 및 HttpURLConnection 설정
            URL url = new URL(soapEndpointUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // HTTP 메서드와 헤더 설정
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
            conn.setRequestProperty("SOAPAction", soapAction);
            conn.setDoOutput(true);

            // 요청 보내기
            try (OutputStream os = conn.getOutputStream()) {
                byte[] input = soapXml.getBytes("utf-8");
                os.write(input, 0, input.length);
            }

            // 응답 처리
            int responseCode = conn.getResponseCode();
            result.put("code", responseCode);

            // 성공 및 에러 메시지 처리
            if (responseCode == HttpURLConnection.HTTP_OK) { // 성공 응답
                responseXML = readResponse(conn.getInputStream());
                System.out.println("Response: " + responseXML);
            } else { // 에러 응답
                responseXML = readResponse(conn.getErrorStream());
                System.out.println("Error: " + responseXML);
            }

            // XML 문서 파싱
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(new InputSource(new StringReader(responseXML)));
            NodeList nodeList = doc.getElementsByTagNameNS("http://UPDATE.UPDATE_Req", "UPDATE_Resp");
            Element respElement = (Element) nodeList.item(0);
            String returnCode = respElement.getElementsByTagName("returnCode").item(0).getTextContent();
            String msg = respElement.getElementsByTagName("msg").item(0).getTextContent();

            return ResponseEntity.ok(result);

            } catch (Exception e) {
                log.error("SOAP 요청 에러 = {}", e.getMessage());
                return ResponseEntity.ok(result);
            }
    }

위 예제와 같이 SOAP로 요청할 xml을 String으로 만들고 요청을 보낸뒤 응답 xml을 파싱해 처리 하였다. 실제로는 응답받은 xml을 파싱해 성공인지를 체크하고 다른 비즈니스로직을 실행하지만 더미 예제로 변경 하다보니 사용시에는 요청 스펙에 맞게 변경이 필요하다.

restAPI와 json이였다면 훨씬 수월했을 작업이 너무 길어지고 해결하는 과정에서 삽질을 많이 했다. 이 글에는 없지만 실제 개발중에는 수신을 위한 웹서비스를 별도로 만들어 spring boot가 동작하는 톰캣 이외에 톰캣에서 실행하는게 필요하는 등 수월하지 않았다.

'BACKEND' 카테고리의 다른 글

Nest.js 공부하기 (1)  (3) 2024.11.04
Prisma 공부하기  (3) 2024.10.20
SpringBoot + Next.js 프로젝트 회고  (1) 2024.01.11
SpringBoot 공부하기 2편  (1) 2024.01.09
SpringBoot 공부하기 1편  (1) 2024.01.02