iframe으로 특정서버의 화면을 띄워야하는 일이 생겼다.

현재 운영되고 있는 서버에 추가적으로 batch Application을 올리고 iframe으로 호출하는 방식을 사용하기로 했다.

 


 

서버구성

 

 


문제1. batch 내부의 cookie 사용이 안되어 url에 jsessionId가 붙어감

예시로 /batch;jsessionid=~~~~~~~  형태로 잘못된 url이 생성되었다. 그로인데 Controller를 잘못찾아가는 문제가 발생

 

원인을 찾아보니 로컬에서 테스트 할 때 브라우저의 url을 localhost:8080~~~ 를 사용하였고 iframe의 주소는 
<iframe src="127.0.0.1:8099"..... 형식으로 사용하였기 때문에 발생

 

둘 다 같은 localhost로 변경하여 사용 하니 문제를 해결하였다.
다만 이로인해 운영에서 iframe으로 <iframe src="IP:PORT" ..... 구조를 사용 할수 없을 듯하였다.

 

운영에서는 <iframe src="도메인/batch" 형태로 사용하기로 한다.

 

 


문제2. nginx에 도메인/batch로 들어오면 2번 서버의 8099포트로 보내야한다.

 

배치서버는 1대지만 iframe 껍데기를 호출하는 서버는 2대 이다. 그렇다면 도메인/batch를 접속하게 된다면 LB를 통해
1번 서버 2번 서버 어느곳으로 갈지 알 수 없다.

 

그렇기 때문에 nginx에서 2번서버의 8099 포트로 이동 할수 있도록 proxy를 구성해야한다.

 

이를 위해 nginx의 default.conf를 수정

 

1번 서버, 2번서버 모두  location /batch/를 추가하여 어떤 서버로 요청이 오더라도 2번서버의 8099를 바라볼 수 있도록 구성

......

upstream backend{
	hash #remote_addr;
    server 1번서버아이피:8080;
    server 2번서버아이피:8080;
    
    keepalive 1024;
}

server {
	listen 443 lls;
    .... 
    기타 nginx 설정들, ssl 포함 
    
    ....
    
    location /batch/ {
    	proxy_pass http://2번서버 IP:8099;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location / {
    	proxy_pass http://backend;
    }

 

 


문제3. https에서 http의 iframe을 사용 할 수 없음

브라우저에서 iframe으로 데이터를 불러올 때 데이터가 보이지 않아 console 을 확인해보니

 

에러로그가 발생

was loaded over an insecure connection. this file should be served over https

 

찾아보니 https 도메인을 사용하며 http를 iframe으로 사용할 수 없어서 발생하는 에러로그 

 

아마 배치 application에 

return "redirect:/" 와 같이 302 redirect를 발생시키며 생기는 문제로 보임.

 

nginx의 batch location에 강제로 http를 https로 변환해주도록 설정 추가

location /batch/ {
...
  proxy_redirect http:// https://
...
}

서버에 ssh로 접속 후 프로그램을 실행 후 ssh를 끊게 되면 해당 프로세스는 종료된다.

 

이때 사용 할 수 있는 명령어가 nohup이다

nohup java -jar -Dspring.profiles.active=dev test.jar 1> /dev/null 2>& &

 

nohup을 사용하면 nohup.out이라는 로그 파일이 생성되는데 이를 생성하지 않고자 한다면 

1> /dev/null 2>& 를 사용하도록 하자

package com.portfolio.dto;


import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

/*
    * API 요청 결과를 담는 DTO
    * date: 2024-07-26
    * author: 김만기
    ====== Annotation description ======
    Schema: swagger 문서 작성을 위한 annotation
 */
@Getter
@Setter
public class ApiResultDto<T> {

    @Schema(description = "결과 코드", example = "200")
    private String resultCode;
    @Schema(description = "결과 메시지", example = "성공")
    private String resultMessage;
    @Schema(description = "추가 데이터")
    private T data;
}

normal-board의 프로젝트 구조

NormalBoard 패키지구조


main 프로젝트의 application.porperites(JPA 및 DB 설정)

server.port = 8080


# JPA Configuration
spring.datasource.url=jdbc:mariadb://localhost:3306/portfolio
spring.datasource.username=adm
spring.datasource.password=123
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
logging.level.org.hibernate.type.descriptor.sql=trace

 

 


BaseEntity(테이블 공통) 및 NormalBoardEntity 

BaseEntity

package com.portfolio.entity;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

import java.time.LocalDateTime;

/*
    * date : 2024.07.26
    * author : 김만기
    * Base Entity: 모든 엔티티의 공통 필드를 정의한 추상 클래스

    * ====== Annotation description ======
    MappedSuperclass: jpa에서 상속 시 상속 받은 값들을 컬럼으로 사용하기 위함
    SuperBuilder: extends한 클레스에 builder를 사용하기 위함
    Column: 컬럼명, 길이, notNull 등의 설정

    * ====== field description ======
    firstRegTime: 최초 등록 시간
    lastUpdTime: 마지막 수정 시간
    firstRegUser: 최초 등록 사용자
    lastUpdUser: 마지막 수정 사용자
 */

@MappedSuperclass
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public abstract class BaseEntity {
    @Column(nullable = false)
    LocalDateTime firstRegTime;
    @Column(nullable = false)
    LocalDateTime lastUpdTime;
    @Column(length = 15, nullable = false)
    String firstRegUser;
    @Column(length = 15, nullable = false)
    String lastUpdUser;
}

 NormalBoardEntity

package com.portfolio.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;


/**
 * 일반 게시판 Entity
 * date : 2024.07.26
 * author : 김만기
 ====== Annotation description ======
 Entity: JPA Entity로 선언
 SuperBuilder: 상속 받는 값을을 builder로 사용하기위해 필요
 Table: 테이블 명을 클래스명과 별도로 세팅, index 설정
 Id: pk로 사용할 변수 선언
 GeneratedValue: pk값 자동생성 전략을 선택
 Column: 컬럼명, 길이, notNull 등의 설정
 ====== field description ======
 boardId: 게시판 아이디
 title: 게시판 제목
 content: 게시판 내용
 */

@Entity
@Getter
@Setter
@SuperBuilder
@Table(name = "tbl_normal_board", indexes =
        @Index(name = "tbl_normal_board_idx_01", columnList = "first_reg_time")
)
@NoArgsConstructor
@AllArgsConstructor

public class NormalBoard extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String boardId;

    @Column(length = 30, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;
}

 

실행후 JPA로 Table 생성 확인

 


NormalBoardRepo

package com.portfolio.repo;

import com.portfolio.entity.NormalBoard;
import org.springframework.data.jpa.repository.JpaRepository;

/*
    * date: 2024-07-26
    * author: 김만기
    * JpaRepository를 상속받아 NormalBoard 엔티티를 관리하는 레포지토리
 */
public interface NormalBoardRepo extends JpaRepository<NormalBoard, String> {
}

 


CreateNormalBoardDto

package com.portfolio.Dto;

import com.portfolio.entity.NormalBoard;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;


/**
 * 일반 게시판 DTO
 date : 2024.07.26
 author : 김만기

 ====== Annotation description ======
 Schema: swagger 문서 작성을 위한 annotation

 ====== field description ======
 title: 게시판 제목
 content: 게시판 내용

 ====== method description ======
 toEntity: DTO를 DB에 적재하기 위한 Entity로 변환

 ====== etc description ======
 entity를 사용하고 싶었으나 entity를 사용하면 Swagger의 example Value가 모든 필드를 표기하는 문제가 있어서 DTO를 사용

 */
@Getter
@Setter
public class CreateNormalBoardDto {
    @Schema(description = "게시판 제목", example = "게시판 제목")
    private String title;

    @Schema(description = "게시판 내용", example = "게시판 내용")
    private String content;

    public NormalBoard toEntity() {
        return NormalBoard.builder()
                .title(title)
                .content(content)
                .build();
    }
}

 

NormalBoardService

package com.portfolio.service;

import com.portfolio.entity.NormalBoard;

import java.util.List;
/*
    * 일반 게시판 서비스 인터페이스
    * date: 2024-07-26
    * author: 김만기
    ====== method description ======
    * insertNormalBoard: 일반 게시판 등록
 */
public interface NormalBoardService {
    public void insertNormalBoard(NormalBoard board);

    public List<NormalBoard> selectAllBoardList();
}

 

NormalBoardServiceImpl

package com.portfolio.serviceimpl;

import com.portfolio.entity.NormalBoard;
import com.portfolio.repo.NormalBoardRepo;
import com.portfolio.service.NormalBoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
/*
    * 일반 게시판 서비스 구현체
    * date: 2024-07-26
    * author: 김만기
    *
    * ====== Annotation description ======
    * Service: 서비스 빈으로 등록
    *
    * ====== method description ======
    * insertNormalBoard: 일반 게시판 등록
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class NormalBoardServiceImpl implements NormalBoardService {
    private final NormalBoardRepo boardRepo;

    @Override
    public void insertNormalBoard(NormalBoard board) {
        try {
            boardRepo.save(board);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public List<NormalBoard> selectAllBoardList() {
        return boardRepo.findAll();
    }
}

 

ApiResultDto

 

 

NormalBoardController

package com.portfolio.controller;

import com.portfolio.Dto.CreateNormalBoardDto;
import com.portfolio.dto.ApiResultDto;
import com.portfolio.entity.NormalBoard;
import com.portfolio.service.NormalBoardService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

/*
  * 일반 게시판 컨트롤러
  * date: 2024-07-26
  * author: 김만기

  *  ====== Annotation description ======
  * Tag: swagger 문서 작성을 위한 annotation(NormalBoardController를 한 그룹으로 세팅)
  * Operation: swagger 문서 작성을 위한 annotation(메소드별 설명 작성)
 */
@RestController
@RequestMapping("/api/normal-board")
@RequiredArgsConstructor
@Tag(name = "NormalBoard", description = "일반 게시판 관련 API")
public class NormalBoardController {

    private final NormalBoardService normalBoardService;

    @PostMapping("/insert")
    @Operation(summary = "게시물 등록")
    public ResponseEntity<ApiResultDto<NormalBoard>> insertNormalBoard(
            @RequestBody CreateNormalBoardDto board
    ) {
        ApiResultDto<NormalBoard> result = new ApiResultDto<>();
        try {
            NormalBoard normalBoard = board.toEntity();

            normalBoard.setFirstRegTime(LocalDateTime.now());
            normalBoard.setLastUpdTime(LocalDateTime.now());

            // 로그인 기능이 없기 때문이 임시세팅
            normalBoard.setFirstRegUser("admin");
            normalBoard.setLastUpdUser("admin");

            normalBoardService.insertNormalBoard(normalBoard);
            result.setData(normalBoard);
            result.setResultCode("200");
            result.setResultMessage("Success");
        } catch (Exception e) {
            result.setResultCode("500");
            result.setResultMessage("Fail");
        }

        return new ResponseEntity<>(result, result.getResultCode().equals("200") ? org.springframework.http.HttpStatus.OK
                : org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 


TEST

Example Value default 값 확인

 

Insert 쿼리 확인

 

임시 Select 쿼리를 사용한 조회

 

 


https://github.com/Kmmanki/portfolio_was

 

GitHub - Kmmanki/portfolio_was: portfolio

portfolio. Contribute to Kmmanki/portfolio_was development by creating an account on GitHub.

github.com

 

let data = [
  {"id": "ROOT", "name" : "ROOT", "upperId": '', "level" : 0, "order": 0},

  {"id": "HOME",   "name" : "HOME",   "upperId": 'ROOT', "level" : 1, "order": 1},
  {"id": "MOBILE", "name" : "MOBILE", "upperId": 'ROOT', "level" : 1, "order": 2},
  {"id": "COMMON", "name" : "COMMON", "upperId": 'ROOT', "level" : 1, "order": 3},

  {"id": "cH1", "name" : "카테고리1", "upperId": 'HOME', "level" : 2, "order": 1},
  {"id": "cH2", "name" : "카테고리2", "upperId": 'HOME', "level" : 2, "order": 2},
  {"id": "cM3", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 3},
  {"id": "cM4", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 4},
  {"id": "cM5", "name" : "카테고리3", "upperId": 'MOBILE', "level" : 2, "order": 5},

  {"id": "c1s1", "name" : "시나리오1", "upperId": 'cH1', "level" : 3, "order": 2},
  {"id": "c1s2", "name" : "시나리오2", "upperId": 'cH1', "level" : 3, "order": 1},
  {"id": "c1s3", "name" : "시나리오3", "upperId": 'cH1', "level" : 3, "order": 3},
  {"id": "c2s4", "name" : "시나리오4", "upperId": 'cH2', "level" : 3, "order": 4},
  {"id": "c2s5", "name" : "시나리오5", "upperId": 'cH2', "level" : 3, "order": 6},
  {"id": "c2s6", "name" : "시나리오6", "upperId": 'cH2', "level" : 3, "order": 5},
  {"id": "c2s7", "name" : "시나리오7", "upperId": 'cH2', "level" : 3, "order": 7},
  {"id": "c3s8", "name" : "시나리오8", "upperId": 'cM3', "level" : 3, "order": 8},
]

function dataSort(data){
  return data.sort((a, b) => a.level - b.level || a.order - b.order )
}

function makeHierachy(data) {
  // root를 제외한 모든 id 별 포함되는데이터 추출(upperId를 사용)
  const dataMap = data.filter(x => x.level !== 0).reduce((acc, x) => {
    if (!acc[x.upperId]) {
      acc[x.upperId] = []
    }
    // order를 재정의
    x.order = acc[x.upperId].length + 1
    acc[x.upperId].push(x)

    return acc
  }, {})

  // 각 데이터들에 children에 데이터 할당
  for(const d of data) {
    if (dataMap[d.id]) {
      d.children = dataMap[d.id]
    } else {
      delete d.children
    }

  }

}

// 데이터 조회
function look(data) {
  console.log(String(data.name).padStart(data.level * 5)  )
  if(!data.children){
    return
  }
  for(const d of data.children){
    look(d)
  }
}

dataSort(data)
makeHierachy(data)
console.log(data)
// 루트부터 깊이 우선 탐색으로 조회
look(data[0])

 

 

+ Recent posts