본문 바로가기
Spring( Framework )

WebSquare + Spring 연동

by 헬로제이콥 2025. 7. 2.

WebSquare + Spring 연동 예제 소스 코드 (MyBatis X)

이 예제는 "사용자 정보 조회" 및 "사용자 정보 등록" 기능을 구현합니다.

1. WebSquare 화면 (클라이언트)

userList.xml (화면 정의)

XML
 
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ev="http://www.w3.org/2001/xml-events"
    xmlns:w2="http://www.websquare.com/2001/WebSquare"
    xmlns:xq="http://www.w3.org/2005/xpath-functions">
    <head>
        <w2:type>DEFAULT</w2:type>
        <w2:buildDate/>
        <w2:exampleDataMap/>
        <w2:docType>HTML</w2:docType>
        <w2:uiInfo>
            <w2:w2ui chartid="" ev:onpageload="scwin.onpageload" gcmode="true" weak="false"/>
        </w2:uiInfo>
        <script type="javascript" lazy="false" src="/ui/user/userList.js"></script>
    </head>
    <body>
        <w2:dataMap baseNode="map" id="dataMap_search">
            <w2:udc_info>
                <w2:column dataType="text" name="userName"></w2:column>
            </w2:udc_info>
        </w2:dataMap>
        <w2:dataMap baseNode="map" id="dataMap_user">
            <w2:udc_info>
                <w2:column dataType="text" name="userId"></w2:column>
                <w2:column dataType="text" name="userName"></w2:column>
                <w2:column dataType="text" name="userEmail"></w2:column>
            </w2:udc_info>
        </w2:dataMap>
        <w2:dataList baseNode="list" id="dataList_userList" saveRemovedData="true">
            <w2:udc_info>
                <w2:column dataType="text" name="userId"></w2:column>
                <w2:column dataType="text" name="userName"></w2:column>
                <w2:column dataType="text" name="userEmail"></w2:column>
            </w2:udc_info>
        </w2:dataList>

        <w2:input id="input_searchUserName" style="width: 150px;" ref="dataMap_search.userName"></w2:input>
        <w2:button id="btn_search" style="width: 80px;height: 23px;" ev:onClick="scwin.btn_search_onClick" value="조회"></w2:button>
        <w2:button id="btn_new" style="width: 80px;height: 23px;" ev:onClick="scwin.btn_new_onClick" value="신규등록"></w2:button>

        <w2:grid id="grid_user" scrollByColumn="false" scrollByCell="false"
                 dataList="dataList_userList" style="width: 400px;height: 200px;">
            <w2:caption/>
            <w2:header style="">
                <w2:row style="">
                    <w2:column displayMode="label" id="column1" inputType="text" style="height:20px;" value="아이디" width="100"></w2:column>
                    <w2:column displayMode="label" id="column2" inputType="text" style="height:20px;" value="이름" width="150"></w2:column>
                    <w2:column displayMode="label" id="column3" inputType="text" style="height:20px;" value="이메일" width="150"></w2:column>
                </w2:row>
            </w2:header>
            <w2:gBody style="">
                <w2:row style="">
                    <w2:column displayMode="label" id="userId" inputType="text" width="100"></w2:column>
                    <w2:column displayMode="label" id="userName" inputType="text" width="150"></w2:column>
                    <w2:column displayMode="label" id="userEmail" inputType="text" width="150"></w2:column>
                </w2:row>
            </w2:gBody>
        </w2:grid>

        <w2:popup id="popup_userReg" style="width: 300px; height: 200px;">
            <w2:anchor winType="window" xpath="html"/>
            <w2:contents>
                <w2:input id="input_userId_reg" ref="dataMap_user.userId" style="width: 150px;"></w2:input>
                <w2:input id="input_userName_reg" ref="dataMap_user.userName" style="width: 150px;"></w2:input>
                <w2:input id="input_userEmail_reg" ref="dataMap_user.userEmail" style="width: 150px;"></w2:input>
                <w2:button id="btn_save" style="width: 80px;height: 23px;" ev:onClick="scwin.btn_save_onClick" value="저장"></w2:button>
            </w2:contents>
        </w2:popup>
    </body>
</html>

userList.js (JavaScript 로직)

JavaScript
 
scwin.onpageload = function() {
    // 페이지 로드 시 초기 데이터 조회 (선택 사항)
    // scwin.searchUserList();
};

// 사용자 조회 버튼 클릭 이벤트
scwin.btn_search_onClick = function() {
    scwin.searchUserList();
};

// 사용자 조회 함수 (READ)
scwin.searchUserList = function() {
    var searchParam = dataMap_search.getJSON(); // dataMap_search의 데이터를 JSON 형태로 가져옴

    WebSquare.request.execute( {
        id : "searchUser",
        action : "/api/users", // 조회 API URL
        method : "GET",
        mode : "asynchronous",
        mediaType : "application/json",
        requestData : searchParam, // 검색 조건 JSON
        successCallback : function(res) {
            // 서버 응답 (JSON)을 dataList_userList에 바인딩
            dataList_userList.setJSON(res.responseJSON.userList); // 서버 응답에서 'userList' 키의 값을 바인딩
            WebSquare.util.printStackTrace("사용자 목록 조회 성공: " + JSON.stringify(res.responseJSON));
        },
        errorCallback : function(res) {
            WebSquare.util.printStackTrace("사용자 목록 조회 실패: " + JSON.stringify(res.responseJSON));
            alert("사용자 목록 조회 실패했습니다.");
        }
    } );
};

// 신규등록 버튼 클릭 이벤트
scwin.btn_new_onClick = function() {
    dataMap_user.setEmptyAll(); // 등록용 dataMap 초기화
    $p.popup("popup_userReg").open(); // 등록 팝업 열기
};

// 사용자 저장 버튼 클릭 이벤트 (CREATE)
scwin.btn_save_onClick = function() {
    var userData = dataMap_user.getJSON(); // dataMap_user의 데이터를 JSON 형태로 가져옴

    if (!userData.userId || !userData.userName) {
        alert("아이디와 이름은 필수 입력 항목입니다.");
        return;
    }

    WebSquare.request.execute( {
        id : "saveUser",
        action : "/api/users", // 등록 API URL
        method : "POST",
        mode : "asynchronous",
        mediaType : "application/json",
        requestData : userData, // 사용자 정보 JSON
        successCallback : function(res) {
            alert("사용자 정보가 성공적으로 저장되었습니다.");
            $p.popup("popup_userReg").close(); // 팝업 닫기
            scwin.searchUserList(); // 목록 새로고침
        },
        errorCallback : function(res) {
            WebSquare.util.printStackTrace("사용자 저장 실패: " + JSON.stringify(res.responseJSON));
            alert("사용자 저장에 실패했습니다.");
        }
    } );
};

2. 백엔드 (Spring Boot 기준)

UserDto.java (DTO - Data Transfer Object)

Java
 
package com.example.websquareexample.dto;

public class UserDto {
    private String userId;
    private String userName;
    private String userEmail;

    // Getters and Setters (생성자를 사용해도 됩니다.)
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserEmail() {
        return userEmail;
    }

    public void setUserEmail(String userEmail) {
        this.userEmail = userEmail;
    }

    @Override
    public String toString() {
        return "UserDto{" +
               "userId='" + userId + '\'' +
               ", userName='" + userName + '\'' +
               ", userEmail='" + userEmail + '\'' +
               '}';
    }
}

UserController.java (Controller)

Java
 
package com.example.websquareexample.controller;

import com.example.websquareexample.dto.UserDto;
import com.example.websquareexample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController // @Controller + @ResponseBody (자동 JSON 변환)
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    // 사용자 목록 조회 (READ)
    @GetMapping("/users")
    public ResponseEntity<Map<String, Object>> getUsers(@RequestParam(required = false) String userName) {
        System.out.println("[Controller] getUsers 요청. 검색 조건: userName=" + userName);

        // 서비스 계층 호출
        List<UserDto> userList = userService.getUsers(userName);

        // WebSquare에 맞춰 Map 형태로 응답 구성 (userList 키로 List를 담음)
        Map<String, Object> response = new HashMap<>();
        response.put("userList", userList);
        response.put("message", "사용자 목록 조회 성공");

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    // 사용자 정보 등록 (CREATE)
    @PostMapping("/users")
    public ResponseEntity<Map<String, String>> createUser(@RequestBody UserDto userDto) {
        System.out.println("[Controller] createUser 요청. UserDto: " + userDto.toString());

        try {
            // 서비스 계층 호출
            userService.createUser(userDto);
            Map<String, String> response = new HashMap<>();
            response.put("message", "사용자 정보 등록 성공");
            return new ResponseEntity<>(response, HttpStatus.CREATED); // 201 Created
        } catch (Exception e) {
            System.err.println("[Controller] 사용자 등록 실패: " + e.getMessage());
            Map<String, String> errorResponse = new HashMap<>();
            errorResponse.put("message", "사용자 등록 실패: " + e.getMessage());
            return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); // 400 Bad Request
        }
    }

    // UPDATE, DELETE는 유사한 방식으로 구현됩니다.
    // @PutMapping("/users/{userId}")
    // @DeleteMapping("/users/{userId}")
}

UserService.java (Service)

Java
 
package com.example.websquareexample.service;

import com.example.websquareexample.dao.UserDao;
import com.example.websquareexample.dto.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserDao userDao; // DAO 의존성 주입

    // 사용자 목록 조회 비즈니스 로직 (READ)
    public List<UserDto> getUsers(String userName) {
        System.out.println("[Service] getUsers 호출. 검색 조건: " + userName);
        // 비즈니스 로직: 예를 들어, 검색 조건 가공, 권한 확인 등
        // 현재는 간단히 DAO에 그대로 전달
        return userDao.selectUsers(userName);
    }

    // 사용자 정보 등록 비즈니스 로직 (CREATE)
    @Transactional // 트랜잭션 관리 (여러 DB 작업이 묶일 때 사용)
    public void createUser(UserDto userDto) {
        System.out.println("[Service] createUser 호출. 등록할 사용자: " + userDto.toString());
        // 비즈니스 로직: 예를 들어, 아이디 중복 확인, 데이터 유효성 검사 등
        if (userDao.selectUserById(userDto.getUserId()) != null) {
            throw new RuntimeException("이미 존재하는 사용자 ID입니다: " + userDto.getUserId());
        }
        userDao.insertUser(userDto);
    }

    // UPDATE, DELETE에 대한 서비스 메서드도 유사하게 구현
}

UserDao.java (DAO - Data Access Object)

MyBatis를 사용하지 않으므로, 여기서는 간단하게 메모리 내(In-Memory) 데이터 저장소를 구현하여 DB 연동을 흉내 냅니다. 실제 DB 연동 시에는 JDBC 템플릿, JPA 등을 사용하여 구현합니다.

Java
 
package com.example.websquareexample.dao;

import com.example.websquareexample.dto.UserDto;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Repository
public class UserDao {

    // 실제 DB 대신 사용할 In-Memory 저장소 (스레드 안전한 Map)
    private static final Map<String, UserDto> inMemoryDb = new ConcurrentHashMap<>();

    // 초기 데이터 추가 (테스트용)
    static {
        inMemoryDb.put("user01", createUserDto("user01", "김철수", "kim@example.com"));
        inMemoryDb.put("user02", createUserDto("user02", "이영희", "lee@example.com"));
        inMemoryDb.put("user03", createUserDto("user03", "박민수", "park@example.com"));
    }

    private static UserDto createUserDto(String userId, String userName, String userEmail) {
        UserDto userDto = new UserDto();
        userDto.setUserId(userId);
        userDto.setUserName(userName);
        userDto.setUserEmail(userEmail);
        return userDto;
    }

    // 사용자 목록 조회 (SELECT)
    public List<UserDto> selectUsers(String userName) {
        System.out.println("[DAO] selectUsers 호출. 검색 조건: " + userName);
        if (userName == null || userName.isEmpty()) {
            return new ArrayList<>(inMemoryDb.values()); // 모든 사용자 반환
        } else {
            // 이름으로 필터링
            return inMemoryDb.values().stream()
                    .filter(user -> user.getUserName().contains(userName))
                    .collect(Collectors.toList());
        }
    }

    // 특정 사용자 ID로 조회 (SELECT by ID)
    public UserDto selectUserById(String userId) {
        System.out.println("[DAO] selectUserById 호출. ID: " + userId);
        return inMemoryDb.get(userId);
    }

    // 사용자 정보 삽입 (INSERT)
    public void insertUser(UserDto userDto) {
        System.out.println("[DAO] insertUser 호출. 삽입할 사용자: " + userDto.toString());
        if (inMemoryDb.containsKey(userDto.getUserId())) {
            throw new RuntimeException("이미 존재하는 사용자 ID입니다: " + userDto.getUserId());
        }
        inMemoryDb.put(userDto.getUserId(), userDto);
        System.out.println("[DAO] 사용자 삽입 완료. 현재 총 사용자 수: " + inMemoryDb.size());
    }

    // 사용자 정보 수정 (UPDATE)
    public void updateUser(UserDto userDto) {
        System.out.println("[DAO] updateUser 호출. 수정할 사용자: " + userDto.toString());
        if (inMemoryDb.containsKey(userDto.getUserId())) {
            inMemoryDb.put(userDto.getUserId(), userDto); // 기존 데이터 덮어쓰기
            System.out.println("[DAO] 사용자 수정 완료.");
        } else {
            throw new RuntimeException("존재하지 않는 사용자 ID입니다: " + userDto.getUserId());
        }
    }

    // 사용자 정보 삭제 (DELETE)
    public void deleteUser(String userId) {
        System.out.println("[DAO] deleteUser 호출. 삭제할 ID: " + userId);
        if (inMemoryDb.containsKey(userId)) {
            inMemoryDb.remove(userId);
            System.out.println("[DAO] 사용자 삭제 완료. 현재 총 사용자 수: " + inMemoryDb.size());
        } else {
            throw new RuntimeException("존재하지 않는 사용자 ID입니다: " + userId);
        }
    }
}

소스 분석 및 흐름 파악 방법

위 예제 소스를 가지고 직접 IDE에서 프로젝트를 생성하여 실행해보면서 다음 순서로 분석하세요.

  1. 프로젝트 설정:
    • Spring Boot 프로젝트를 생성합니다 (Java 11 이상 권장).
    • spring-boot-starter-web 의존성을 pom.xml에 추가합니다.
    • 위의 UserDto, UserController, UserService, UserDao 클래스 파일을 적절한 패키지(com.example.websquareexample.dto, .controller, .service, .dao 등)에 생성합니다.
    • Spring Boot 애플리케이션을 실행합니다. (main 메서드가 있는 @SpringBootApplication 클래스 실행)
  2. WebSquare 파일 배치:
    • WebSquare 프로젝트 내에 userList.xml과 userList.js 파일을 적절한 경로(예: ui/user/userList.xml, ui/user/userList.js)에 배치하고 WebSquare 서버를 통해 화면을 띄웁니다.
  3. 흐름 따라가기 (예: 사용자 목록 조회 - READ)
    • WebSquare (userList.xml, userList.js):
      • 브라우저에서 userList.xml 화면을 엽니다.
      • btn_search 버튼의 onClick 이벤트 (scwin.btn_search_onClick)를 확인합니다.
      • scwin.btn_search_onClick 함수 내에서 scwin.searchUserList()를 호출하는 것을 확인합니다.
      • scwin.searchUserList 함수에서 WebSquare.request.execute() 호출을 찾습니다.
      • 여기서 action : "/api/users", method : "GET", requestData : searchParam (dataMap_search의 userName)을 확인합니다.
      • successCallback에서 dataList_userList.setJSON(res.responseJSON.userList)를 통해 서버 응답 중 userList라는 키의 값을 dataList_userList에 바인딩하는 것을 확인합니다.
    • Spring Backend (UserController.java):
      • UserController.java를 열고 @GetMapping("/users") 어노테이션이 붙은 getUsers 메서드를 찾습니다.
      • 메서드의 파라미터 @RequestParam(required = false) String userName을 통해 WebSquare에서 보낸 userName이 여기에 매핑됨을 확인합니다.
      • userService.getUsers(userName)를 호출하여 서비스 계층으로 제어권을 넘기는 것을 확인합니다.
      • 반환된 userList를 Map에 userList라는 키로 담아 ResponseEntity로 반환하는 것을 확인합니다. (이것이 WebSquare successCallback의 res.responseJSON.userList에 해당)
    • Spring Backend (UserService.java):
      • UserService.java를 열고 getUsers(String userName) 메서드를 찾습니다.
      • 여기서 userDao.selectUsers(userName)를 호출하여 DAO 계층으로 제어권을 넘기는 것을 확인합니다.
    • Spring Backend (UserDao.java):
      • UserDao.java를 열고 selectUsers(String userName) 메서드를 찾습니다.
      • 여기서는 inMemoryDb에서 userName에 따라 필터링하여 UserDto 리스트를 반환하는 "데이터 조회" 로직을 확인합니다.
  4. 흐름 따라가기 (예: 사용자 등록 - CREATE)
    • WebSquare (userList.xml, userList.js):
      • btn_new 클릭 -> popup_userReg 열림 -> dataMap_user에 아이디, 이름, 이메일 입력.
      • btn_save 클릭 -> scwin.btn_save_onClick 호출.
      • dataMap_user.getJSON()으로 입력된 데이터를 userData 변수에 저장.
      • WebSquare.request.execute() 호출에서 action : "/api/users", method : "POST", requestData : userData를 확인합니다.
    • Spring Backend (UserController.java):
      • UserController.java를 열고 @PostMapping("/users") 어노테이션이 붙은 createUser 메서드를 찾습니다.
      • 메서드의 파라미터 @RequestBody UserDto userDto를 통해 WebSquare에서 보낸 JSON 데이터가 UserDto 객체로 매핑되는 것을 확인합니다.
      • userService.createUser(userDto)를 호출하여 서비스 계층으로 제어권을 넘기는 것을 확인합니다.
      • 성공 시 HttpStatus.CREATED (201)를 반환하는 것을 확인합니다.
    • Spring Backend (UserService.java):
      • UserService.java를 열고 createUser(UserDto userDto) 메서드를 찾습니다.
      • @Transactional 어노테이션으로 트랜잭션이 적용됨을 확인합니다.
      • userDao.selectUserById()로 ID 중복 확인 후 userDao.insertUser(userDto)를 호출하여 DAO 계층으로 제어권을 넘기는 것을 확인합니다.
    • Spring Backend (UserDao.java):
      • UserDao.java를 열고 insertUser(UserDto userDto) 메서드를 찾습니다.
      • 여기서는 inMemoryDb에 UserDto 객체를 추가하는 "데이터 삽입" 로직을 확인합니다.

clo.omn 파일 관련

이 예제에서는 clo.omn 파일에 대한 실제 구현은 포함하지 않습니다. 하지만 만약 프로젝트에 해당 파일이 있다면:

  1. 파일 열기: 해당 파일을 텍스트 에디터로 열어서 내부 구조(XML, JSON 등)와 키워드를 확인합니다.
  2. 전역 검색: 백엔드 Java 코드(*.java)와 WebSquare JavaScript (*.js)에서 clo.omn 문자열을 전역 검색합니다. 어떤 코드에서 이 파일을 읽고 파싱하는지, 그리고 어떤 목적으로 사용하는지 (Clova, chatbot, OCR 관련 로직)를 찾아내면 파일의 역할을 정확히 파악할 수 있습니다.

이 간단한 예제를 통해 WebSquare와 백엔드 간의 데이터 흐름, 그리고 컨트롤러-서비스-DAO 계층 간의 역할 분담을 명확히 이해하실 수 있을 겁니다. 직접 코드를 실행하고 디버깅해보는 것이 가장 중요합니다!