왜 React인가?

React는 Facebook이 만든 JavaScript 라이브러리로, 복잡한 웹 사이트의 화면을 컴포넌트 단위로 나누어 개발하고, 데이터 변경 시 화면을 빠르고 효율적으로 업데이트 하는데 특화되어 있다.Codecademy에서는 React의 강점을 아래 다섯 가지로 얘기하고 있다.

  1. 빠르다(Fast): 애플리케이션의 속도와 성능을 최적화
    • Virtual DOM: 실제 DOM이 완성된 집이라면, Virtual DOM은 집에 설계도이다. 설계도를 고치는건 쉽고 빠르지만, 완성된 집을 고치는건 어렵다. 이와 같이 React는 메모리상의 Virtual DOM을 통해 변경된 부분만 계산하여 실제 화면에 반영한다. 불필요한 연산을 최소화하여 사용자에게 빠르고 부드러운 경험을 제공하게 된다.
  2. 모듈화(Modular): 화면의 요소를 작고 독립적인 조각인 컴포넌트로 나눔
    • 한 번 만든 버튼, 헤더, 입력 폼 등을 여러 페이지에서 계속 재사용할 수 있어 재사용성이 높고, 코드가 거대한 하나의 파일이 아니라 기능별로 쪼개져 있어 코드를 읽고 이해하기 쉽다.
  3. 확장성(Scalable): 작은 프로젝트가 거대한 엔터프라이즈급 애플리케이션으로 커져도 관리가 용이
    • 수천 개의 컴포넌트가 있어도 모듈화된 구조 덕분에 특정 기능만 수정하거나 업데이트하기가 쉽고, 데이터가 부모에서 자식으로 한 방향으로만 흐르기 때문에, 데이터의 흐름을 추적하기 쉬워 버그를 찾고 고치는 데 유리하다.
  4. 유연성(Flexible): 프레임워크가 아니라 라이브러리이기 때문에 제약이 적음
    • 기존에 만들어진 사이트의 일부분에만 적용할 수도 있을 정도로 어디든 적용 가능하고, React Native로 iOS와 Andorid 모바일 앱까지 개발할 수 있다.
  5. 인기(Poopular): 전 세계에서 가장 많이 사용되는 라이브러리 중 하나
    • 거대한 커뮤니티와 풍부한 생태계, 수요가 높아 취업 시장에서도 유리하다. (아...)

JSX란?

JSX(JavaScript XML)는 React에서 UI를 구성할 때 사용하는 문법으로, JavaScript의 모든 기능을 갖춘 확장 문법이다. 겉보기에는 HTML과 거의 똑같으나 실제로는 JavaScript이다. React는 별도의 HTML 파일과 JS 파일을 분리하는 대신에 렌더링 로직과 비즈니스 로직이 본질적으로 결합되어 있다고 보며, JSX를 사용한는 이유는 가독성, 생산성, 안정성 등에 있다.

  • 가독성: 복잡한 createElement 함수를 쓰는 것보다 HTML 태그 형태가 구조를 파악하기 쉽다.
  • 생산성: HTML 구조와 JavaScript 로직을 한 공간에서 자유롭게 섞어 쓸 수 있다.
  • 안정성: JSX에 포함된 값을 렌더링하기 전에 이스케이프 처리하므로 XSS 공격을 방지하는 데 도움이 된다.

브라우저는 JSX를 이해하지 못하고 표준 JavaScript만 이해할 수 있다. 따라서 Babel 같은 트랜스파일러(컴파일러)가 JSX 코드를 브라우저가 이해할 수 있는 일반 JavaScript 코드로 변환해 준다.

필수 규칙 3가지

  1. 하나의 부모 요소로 감싸야 한다.
    컴포넌트는 반환할 때 반드시 하나의 덩어리여야 하며, 두 개 이상의 태그가 나란히 있다면 div<>...</>(프래그먼트, Fragment)로 감싸주어야 한다.
  2. JavaScript 표현식은 { }를 사용한다.
    JSX 내부에서 변수나 함수 같은 JavaScript 값을 쓸 때는 중괄호로 감싸준다.
  3. 속성 이름은 camelCase를 따른다.
    JSX는 JavaScript이므로, HTML 속성 이름 대신 JavaScript 프로퍼티 이름을 사용해야 한다.

로컬에서 확인하기

$ npm create vite@latest
$ npm install
$ npm run dev

 

Codecademy에서는 내부 셋팅 때문에 한 화면에서 다 처리하는데, Vite에서는 main.jsx에 준비하고 태그 찾는 역할을, App.jsx에 내용을 작성하는 역할이 있는듯 하다. 그래서 조금 다른 느낌.

 

https://github.com/hongbre/codecademy-project/tree/main/ReactPart1

 

codecademy-project/ReactPart1 at main · hongbre/codecademy-project

Contribute to hongbre/codecademy-project development by creating an account on GitHub.

github.com

 

날려먹은 레거시 시스템 배포 스크립트는 배포 목록 디렉토리에 업로드 된 tar.gz 파일을 각 서버로 전송하고, 서버에서 스크립트를 실행하여 목표 시스템에 배포하는 구조로 되어있었다. 각 서버에 있는 스크립트는 보존하였기 때문에 그 이전 과정까지만 Ansible로 변경했다. Ansible Playbook을 작성하는 건 처음이라 문법을 익히면서 주석으로 달아놨다.

---
- name: 레거시 시스템 배포 # 작업 제목
  hosts: all # 작업 대상 서버 지정으로 인벤토리(hosts.ini) 내부의 모든 서버가 대상
  gather_facts: no # 서버 정보 수집 여부

  vars:
    local_dir: "/home/jboss/deploy" # 단순 변수
    remote_dir: "/home/jboss/deploy"
    remote_script: "deploy.sh"
    system_map: # 복합 변수
      - { key: "foo", group: "foo" }
      - { key: "bar", group: "bar" }
      - { key: "baz", group: "baz" }

  tasks:
    - name: 1. 배포 파일 찾기
      delegate_to: localhost # Ansible을 실행하는 서버에서 실행 
      run_once: true # 딱 한 번만 실행
      find:
        paths: "{{ local_dir }}" # 찾을 위치로 하위 디렉토리까지 찾으려면 recurse: yes 추가 필요
        patterns: # 찾을 패턴
          - "*foo*.tar.gz"
          - "*bar*.tar.gz"
          - "*baz*.tar.gz"
      register: found_files # 파일을 찾으면 path, mode, size 등 ls -l 수준의 정보가 저장

    - name: 2. 파일이 없으면 종료
      fail:
        msg: "오류: 배포할 파일이 없습니다."
      when: found_files.matched == 0 # 위에서 found_files에 아무것도 안 들어가면 matched 값이 0

#    - name: 2-1. found_files 출력
#      debug:
#        var: found_files

    - name: 3-1. 파일 전송 여부 확인 플래그 초기화
      set_fact:
        file_copied: false

    - name: 3-2. 파일 전송
      include_tasks: task_upload_batch.yml # 해당 Task를 반복 실행
      loop: "{{ system_map }}" # system_map을 기준으로 반복
      loop_control:
        loop_var: map_item # 기본적으로 item이라는 편수에 담는데, 겹치면 안 되니까 변수명 변경

    - name: 4. 배포 스크립트 실행
      shell: "cd {{ remote_dir }} && ./{{ remote_script }}"
      register: script_result
      # hostvars[inventory_hostname]은 작업 중인 서버이고 'file_copied'가 정의 되어 있고 그 값이 true이면 스크립트 실행
      when: hostvars[inventory_hostname]['file_copied'] is defined and hostvars[inventory_hostname]['file_copied']

    - name: 5. 실행 결과 확인
      debug:
        msg: "{{ script_result.stdout_lines }}"
      when: script_result is not skipped # 실행했으면 결과 확인

    - name: 6. 배포 파일 정리
      delegate_to: localhost
      run_once: true
      shell: "mv {{ local_dir }}/*.tar.gz {{ local_dir }}/success/"
      when: found_files.matched > 0

 

이건 3-2에서 사용한 include task의 내용이다.

---
- name: 3-2-1. 파일 전송
  copy: # when 조건에 부합하면(AND) 파일 전송
    src: "{{ item.path }}" 
    dest: "{{ remote_dir }}/{{ item.path | basename }}"
  when:
    - map_item.group in group_names # 실행 서버가 해당 group에 포함되는지
    - map_item.key in (item.path | basename) # 대상 파일 이름에 해당 key가포함되는지
  loop: "{{ found_files.files }}"
  register: copy_result # 전송 결과 저장
- name: 3-2-2. 파일 전송 여부 확인 플래그 설정
  set_fact:
    file_copied: true
  # copy_result를 목록으로 받아서 results를 보고, 이 중에changed가 true인 결과만 list로 만들고, 
  # 이게 1개라도 있으면 플래그를 true로 설정
  when: copy_result.results | selectattr('changed', 'equalto', true) | list | length > 0

delegate_to: localhost가 없으면 기본적으로 모든 대상서버에서 작업을 수행한다.

'DevOps > CI.CD' 카테고리의 다른 글

[Ansible] RHEL 7.8 폐쇄망 설치  (0) 2026.01.30
[Bash] Apache 설정 파일 일괄 배포  (0) 2025.05.30

망할. 레거시 시스템 배포하다가 배포 스크립트를 날려먹었다. 근데 또 쉘 스크립트를 짜자니 귀찮고, Ansible 사용을 해보는 것에 목적을 두고 한 번 시작해본다. 설치 되어야하는 서버가 RHEL 7.8 버전에 Python 3.6.8 버전이고, 폐쇄망이다보니 인터넷 되는 환경에서 파일을 받아서 옮겨야 한다.

$ pip download ansible==2.9.27 --no-deps
$ pip download cryptography jinja2 PyYAML paramiko six --platform manylinux2014_x86_64 --python-version 36 --implementation cp --abi cp36m --only-binary=:all:

첫 번째 명령어로 Ansible 2.9.27 버전 소스 코드로 다운로드 하고, 두 번째 명령어로 RHEL 7.8 버전에 맞는 의존성 파일들을 다운로드 한다. 파일 옮기고 설치 할라니 안 된다. crypthography만 오류가 나나 싶다가도, 다르게 시도했을 때 PyNaCl이 문제여서 제미나이를 괴롭힐 수 밖에 없었다. 그래서 아래가 최종이다.

# 1. pip 업데이트 파일
$ pip download pip==21.3.1 setuptools==59.6.0 wheel==0.37.1 --no-deps

# 2. Ansible
$ pip download ansible==2.9.27 --no-deps

# 3. 의존성 (cp36m 태그 강제 지정)
$ pip download cffi==1.15.1 pycparser==2.21 pynacl==1.4.0 cryptography==3.3.2 bcrypt==3.2.0 paramiko==2.7.2 Jinja2==3.0.3 MarkupSafe==2.0.1 PyYAML==5.4.1 six==1.16.0 --platform manylinux2014_x86_64 --python-version 36 --implementation cp --abi cp36m --only-binary=:all: --no-deps

# 4. pip 업데이트
$ python3.6 -m pip install --user pip-21.3.1-py3-none-any.whl setuptools-*.whl wheel-*.whl

# 4-1. pip 업데이트 확인
$ pip3.6 --version

# 5. 나머지 설치
$ pip3.6 install *.whl *.tar.gz --no-index --find-links=.

# 5-1. ansible 설치 확인
$ ansible --version
ansible 2.9.27

이제 Playbook? 준비해서 실행해야지.

'DevOps > CI.CD' 카테고리의 다른 글

[Ansible] 레거시 시스템 배포  (0) 2026.02.05
[Bash] Apache 설정 파일 일괄 배포  (0) 2025.05.30

누군가 예쁘게 수정 해놓은 애플리케이션에 분명 오류가 발생하고 있는데 server.log에 로그가 기록되지 않고 있었다. 첫번째로 확인 한 건 애플리케이션 경로 내의 log4j 설정 파일이다. level이 "off"로 되어 있었고, "INFO"로 수정했다.

<!-- 생략 -->
    <root>
          <!-- 기존 -->
          <level value="off"/>
          <!-- 변경 -->
          <level value="INFO"/>
    </root>
<!-- 생략 -->

 

재기동해도 로그가 기록되지 않는다. jboss-deployment-structure.xml 파일에서 logging subsystem을 제외해보라는 답변을 발견해서 내용을 추가했다.

<!-- 생략 -->
        <exclude-subsystems>
              <subsystem name="logging"/>
        </exclude-subsystems>
<!-- 생략 -->

 

더 찾아봤을 때, JBoss는 기본적으로 애플리케이션의 로그는 server.log에 통합 관리하려고 하다보니 애플리케이션의 log4j 설정보다 JBoss의 logging이 우선순위를 갖게 된다. 그래서 "off"에서 "INFO"로 수정해도 로그가 기록 되지 않았고, logging subsystem을 제외하고 나서야 로그가 기록 되기 시작한 것이었다. 해당 subsystem을 제외하지 않고 등록을 해줘도 되는데, 패키지 경로가 가끔 다른게 튀어나오기도 하다보니 그냥 이대로 두기로.

얼떨결에 JMeter로 부하테스트를 진행하고 있었는데, 연결 시간 초과나 소켓 닫힘 같은 기본적인 네트워크 오류가 특정 시점부터 많이 발생하길래 (1) 스위치나 보안장비에서 동일 IP의 동시 요청 건수를 제한하거나 또는 그런 설정이 있는지 확인하였으나 본인측 요청에 의해 보안장비는 필터링 제외를 시켰고, 스위치는 기본 셋팅이라 제한이 업다고 한다. (2) 당시 Apache의 mpm 설정은 worker로 되어있었고, MaxRequestWorkers는 4,096으로 설정되어 있었다. 모니터링 했을 때 Busy Worker가 폭등하다가 폭락하는 그래프를 보였고 폭락 지점이 네트워크 오류가 다수 발생하는 시점이었다.

 

여기서 부하발생기가 2,000 스레드로 요청을 발생하고 있었는데, 모든 요청을 온전하게 받아들이지 못하고 있었다고 판단을 하였다. 부하발생기의 자원사용률이나 TCP 소켓의 TIME_WAIT 상태는 크게 많지 않았음에도 불구하고 왜 이런 그래프가 반복되는가 고민하는 찰나에 작년 일이 생각났다. Apache를 운영하는 서버의 커널 파라미터를 수정했었다. 당시 사용하던 서버는 Destroyed 되었기 때문에 설정값은 기억나지 않지만(기록이란 참 중요하다) TCP 소켓 reuse와 recycle에 관련했던거 같다.

 

하지만 이게 왠걸 tcp_tw_reusetcp_tw_recycl\은 기본적으로 Outbound 연결에 대해서 효과가 있고 Web 서버 같이 Inbound 연결이 많은 환경에서는 아무 의미가 없다고 한다. 심지어 tcp_tw_recycle은 최신 커널에서는 폐기된 파라미터다. 도대체 작년에는 왜 그런일을 하면서 해골물을 마셨었는지 알 수가 없다.

 

각설하고 이번에는 net.core.somaxconn 파라미터를 수정했다. 연결에 대한 대기열(Queue)인데 기본값인 512로 설정되어 있었다. 부하발생기의 스레드는 반복해서 요청을 보내고 있는데 일부 응답이 늦어지며 연결을 유지하고 있었던 것들에 의해 대기열에 들어갔어야 했다. 하지만 대기열의 크기가 512에 불과하니 연결에 실패하는 등의 오류가 발생했던 것으로 판단되었다. net.core.somaxconn의 값은 Apache의 MaxRequestWorkers의 값과 동일하거나 그 이상으로 설정 해주면 된다.

sudo vim /etc/sysctl.conf

### 파일 내용에서 값을 찾아 수정하거나, 없으면 추가
net.core.somaxconn = 8192

# 바로 적용
sudo sysctl -p

 

그리고 Apache 설정도 바꿔줘야한다. 나중에 헷갈리지 않게 mpm.conf 쪽에다 추가했다. 해당 값은 net.core.somaxconn보다 작거나 같고, MaxRequestWorkers보다 큰 값으로 설정하면 된다. 나는 net.core.somaxconn과 같은 값으로 설정했다.

ListenBacklog 8192

 

Apache도 재기동하고 다시 요청을 보내니 관련 오류가 현저하게 줄어들어 Busy Worker 그래프가 폭락하지는 않았고, 코끼리를 삼킨 보아뱀처럼 그래프가 그려졌다. 아무튼 이를 통해 일부 문제를 해결할 수 있었다. JMeter 다루기도 무엇보다 쉽지가 않네.

다양한 연관관계 매핑

핵심 개념

  1. 다중성(Multiplicity): 다대일(@ManyToOne), 일대다(@OneToMany), 일대일(@OneToOne), 다대다(@ManyToMany)
  2. 방향성(Directionality): 두 엔티티 간의 관계를 한쪽에서만 알게 할 것인지, 양쪽 모두에서 알게 할 것인지
    • 단방향(Undirectional): 한쪽 엔티티만 상대방을 참조 / 설계가 단순하고 직관적
      • 예를 들어, MemberTeam을 알지만 Team은 자신에게 속한 Member들을 모른다
    • 양방향(Bidirectional): 양쪽 엔티티가 서로를 참조 / 신경 쓸 점이 많다
      • 따라서 MemberTeam을 알고, Team도 자신에게 속한 Member 리스트를 가진다
  3. 연관관계의 주인(Owner of the Relationship): 두 엔티티가 서로를 참조할 때, 둘 중 누가 데이터베이스의 외래 키(FK)를 관리하고 변경할지 정해야 한다.
    • 주인(Owner): 외래 키를 직접 관리하는 쪽으로 보통 다대일(N:1) 관계에서 '다(N)'쪽이 주인이된다.
      • @JoinColumn 어노테이션 사용
    • 주인이 아닌 쪽(Non-owner): 외래 키에 영향을 주지 않고, 단순히 읽기 전용으로만 사용한다.
      • mappedBy 속성을 사용해 매핑 필드 지정
      • 양방향 매핑 시 mappedBy 속성을 설정하지 않으면 연결 테이블이 생성되니 주의

다대일(N:1, @ManyToOne)

가장 안정적이고 권장되는 관계이다.

  • 단방향(N:1): '다(N)'쪽이 '일(1)'을 참조한다.
    • MemberTeam
    • Member 엔티티에만 Team 객체 참조가 있다.
    • Team은 자신에게 속한 Memeber를 알 수 없다.
  • 양방향(N:1, 1:N): 양쪽이 서로를 참조한다.
    • MemberTeam
    • 연관관계의 주인은 '다(N)'쪽인 Membber로 @JoinColumn을 사용한다.
    • '일(1)'쪽인 Team은 mappedBy를 사용하여 주인이 아님을 명시한다.

일대다(1:N, @OneToMany)

  • 단방향(1:N): '일(1')쪽이 '다(N)'를 List 등으로 참조한다.
    • TeamList<Memeber>
    • '일(1)'쪽에서 외래 키를 관리하게 되어 UPDATE 쿼리가 추가로 발생하는 등 성능상 문제 가능성
    • 권장하지 않는다.
  • 양방향(1:N, N:1): 다대일 양방향과 동일하다.

일대일(1:1, @OneToOne)

  • 단방향(1:1): 한쪽만 상대방을 참조한다.
    • MemberLocker
    • 주 테이블에 외래 키를 두는 것을 권장한다.
  • 양방향(1:1): 양쪽이 서로를 참조한다.
    • 다대일 양방향과 마찬가지로, 외래 키를 가진 쪽이 연관관계의 주인이 된다.

다대다(N:N, @ManyToMany)

실무에서는 사용을 피해야 하는 관계이다.

  • 관계형 데이터베이스는 다대다 관계를 표현할 수 없어서, 중간에 연결 테이블이 자동으로 생성된다.
  • 이 연결 테이블은 JPA에서 직접 관리할 수 없어, 추가적인 컬럼을 넣거나 세밀한 제어가 불가능하다.
  • 다대다를 일대다, 다대일 관계로 풀어내는 것이 정석이다.

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 5  (1) 2025.08.10
JPA 스터디2 - 4  (3) 2025.08.03
JPA 스터디2 - 3  (5) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13

주말에 압축했던 패키지를 서버로 옮겨서 설치를 시도했다.

# 패키지를 저장할 폴더
$ mkdir /opt/zabbix

# 압축 풀기
$ tar -zxvf zop.tar.gz -C /opt/zabbix

# createrepo_c 먼저 설치
$ sudo rpm -ivh /opt/zabbix/createrepo_c-*.rpm

 

libdrpm.so.0()이 없다면서 오류가 발생했다. 노트북을 챙겨오지 않아서 pkgs.org에서 drpm 패키지를 다운로드 했다.

# drpm 설치
$ sudo dnf install drpm-*.rpm

# createrepo_c 다시 설치
$ sudo dnf install createrepo_c-*.rpm

# 설치 성공 했으니 RPM 파일들이 있는 폴더를 리포지토리로 만들기
$ sudo createrepo_c /opt/zabbix


# 리포지토리 파일 작성
sudo nano /etc/yum.repos.d/zabbix-local.repo

# 파일 내용
[zabbix-local]
name=Zabbix Local Repository
baseurl=file:///opt/zabbix/
enabled=1
gpgcheck=0
# 파일 내용

# 나머지 파일 설치
$ sudo dnf install zabbix-server-mysql zabbix-web-mysql zabbix-apache-conf zabbix-sql-scripts zabbix-agent mariadb-server

 

실행하니까 Updating Subscription Management repositories. Unable to read consumer identity. This system is not registered with an entitlement server. You can use subscription-manager to register. AppStream Errors During downloading metadata for repository 'AppStream': .. 오류가 발생했는데 폐쇄망이라 그런가보다.

# disablerepo="*": 모든 리포지토리 비활성화
# enablerepo="zabbix-local": 방금 만든 리포지토리만 활성화
$ sudo dnf --disablerepo="*" --enablerepo="zabbix-local" install zabbix-server-mysql zabbix-web-mysql zabbix-apache-conf zabbix-sql-scripts zabbix-agent mariadb-server

 

이것도 실행하니까 Error: No available modular metadata for modular package 오류가 발생했다.

# /opt/zabbix 에서 모든 rpm 설치
$ sudo dnf --disablerepo="*" install *.rpm

 

몰랐는데 서버에 이미 MySQL이 설치되어있어서 오류가 났다. 관련 패키지 파일 삭제 후 설치에 성공했다. 근데 나는 MySQL이 설치되어있는지 몰랐기 때문에 계정과 패스워드도 모른다. 그래서 root 패스워드를 초기화 했다.

# 안전모드로 MySQL 시작
$ sudo systemctl stop mysqld
$ sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables"
$ sudo systemctl start mysqld

# 접속 성공
$ sudo mysql -uroot
> FLUSH PRIVILEGES;
> ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
> quit;
# 안전모드 제거
$ sudo systemctl stop mysqld
$ sudo systemctl unset-environment MYSQLD_OPTS
$ sudo systemctl start mysqld

# 변경한 비밀번호로 접속 성공
$ sudo mysql -uroot -p
# Zabbix 데이터베이스 생성(utf8mb4 형식 필수)
> create database zabbix character set utf8mb4 collate utf8mb4_bin;

# Zabbix 사용자 생성
> create user zabbix@localhost identified by 'zabbix';

# Zabbix 사용자에게 zabbix 데이터베이스의 모든 권한을 부여
> grant all privileges on zabbix.* to zabbix@localhost;

# Zabbix 6.0 이상은 SUPERUSER 권한 필요(스키마 가져오고 0으로 초기화)
> set global log_bin_trust_function_creators = 1;

# 변경사항을 적용 후 종료
> flush privileges;
> quit;
# Zabbix 초기 스키마 가져오기
$ zcat /usr/share/zabbix-sql-scripts/mysql/server.sql.gz | mysql --default-character-set=utf8mb4 -uzabbix -p zabbix

# Zabbix 설정 파일 수정(DBPassword)
$ sudo vim /etc/zabbix/zabbix_server.conf

# 파일 수정
# 주석 풀고 비밀번호 입력
DBPassword=zabbix
# 파일 수정

# 서비스 재시작 및 자동 실행 등록
$ sudo systemctl restart zabbix-server zabbix-agent httpd
$ sudo systemctl enable zabbix-server zabbix-agent httpd

 

Zabbix 설치하면서 Apache도 하나 더 설치가 되었는데, 기존에 설치가 된게 있어서 포트 충돌이 났다.

# Apache 포트 수정
$ sudo vim /etc/httpd/conf/httpd.conf

# 파일 수정
Listen 80 → 8080
# 파일 수정

# Apache 서비스 시작
$ sudo systemctl start httpd

 

이제 {서버 IP}:8080/zabbix으로 접근은 되지만 'Minimum required PHP version is 8.0.0' 메시지가 나왔다. 기존에 7 버전대 패키지가 다운로드 되었고, 그걸 설치해서 그런가보다. 또 pkgs에서 PHP 8.0.0 필요한 패키지 다운로드했다.

# 7 버전 삭제
$ sudo dnf remove 'php*'

# 8 버전 설치
$ sudo dnf --disablerepo="*" install php*.rpm

# PHP 재시작 및 자동 실행 등록
$ sudo systemctl restart php-fpm
$ sudo systemctl enable php-fpm

# Apache 서비스 재시작
$ sudo systemctl restart httpd

 

다시 {서버 IP}:8080/zabbix로 접근하면 Zabbix 설치 화면이 나오고, Check of pre-requisites의 모든 항목이 OK로 표시되는지 확인 후 다음으로 넘어가서 DB 정보 입력하고 완료되었다.

어찌저찌 마무리 하였지만.. 에이전트 설치 과정은 다음에

 

'DevOps > 모니터링' 카테고리의 다른 글

Zabbix 설치기 - 1  (2) 2025.08.23

WAS 서버들은 다른 모니터링 툴로 잘 보고 있는데, WEB 서버는 자연스럽게 모니터링을 하지 않고 있음을 문득 깨달아버렸다. 조만간 부하 테스트를 진행해야하는데 putty 여러개 띄워 놓는 것도 그만둘 때가 되었다고 생각했고, Grafana + Prometheus와 Zabbix 중에서 비교하다가 그래도 빠르게 시작할 수 있는 Zabbix로 선택했다.

 

빠르게 Zabbix Appliance(모든 구성 요소가 미리 설치된 가상 머신 이미지)로 시작하려했으나, Zabbix 서버로 사용하려 했던 서버도 이미 가상화 서버이고 중첩 가상화도 지원하지 않아서 포기했다. Docker Container도 제공하고 있어 이쪽으로 할까 했지만 Docker 설정도 안 되어 있어서 직접 설치하기로 했다. 하지만 외부 리포지토리에 접근이 불가한 폐쇄망 서버여서 외부에서 설치 패키지를 가져와 옮겨야한다.

 

https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.9/x86_64/product-software

 

The world's open source leader

Red Hat is the world’s leading provider of open source solutions, using a community-powered approach to provide reliable and high-performing cloud, virtualization, storage, Linux, and middleware technologies. Red Hat also offers award-winning support, tr

www.redhat.com

우선 동일한 환경에서 의존성을 맞추는게 중요하니 RHEL 8.9 이미지를 다운로드했다.

VirtualBox에 무인 설치라는게 생겨서 이미지만 넣으면 자동으로 설치해주더라...

 

VirtualBox 화면에서 하면 복사/붙여넣기가 자유롭지 않으니 파워쉘에서 ssh 연결해서 명령어 입력했다.

# RHEL 8.9용 Zabbix 7.0 LTS 버전 repo 설치
$ sudo rpm -Uvh https://repo.zabbix.com/zabbix/7.0/rhel/8/x86_64/zabbix-release-7.0-1.el8.noarch.rpm
$ sudo dnf clean all

# Zabbix 관련 패키지 및 의존성을 모두 다운로드
$ sudo dnf install --downloadonly --downloaddir=/home/vboxuser/zop \
zabbix-server-mysql \
zabbix-web-mysql \
zabbix-apache-conf \
zabbix-sql-scripts \
zabbix-agent \
mariadb-server \
createrepo_c

# mariadb-server랑 createrepo_c가 No match for argument 가 나오면서 실패

$ sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
This system has no repositories available through subscriptions.

# repo 구독도 실패해서 구독 처리
$ sudo subscription-manager register
Registering to: subscription.rhshttp://m.redhat.com:443/subscription
Username: 
Password:
The system has been registered with ID:
The registered system name is: 

$ sudo subscription-manager attach --auto
Ignoring the request to auto-attach. Attaching subscriptions is disabled for organization "" because Simple Content Access (SCA) is enabled.

# 위의 다운로드 다시 실행하면 정상적으로 다운로드 완료

 

45개 정도의 패키지가 다운로드 되는데 압축해서 서버로 옮기면 된다. 월요일에 출근해서 처리해야한다. 참고로 뒤에 구독 같은거 안 하려면 Rocky Linux 8.9로 하면 된다고 한다.

'DevOps > 모니터링' 카테고리의 다른 글

Zabbix 설치기 - 2  (1) 2025.08.25

객체를 테이블에 맞추어 모델링

예제 시나리오는 다음과 같다.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일(N:1) 관계다.
//회원 객체
@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @Column(name = "TEAM_ID")
    private Long teamId;
    ...
}

//팀 객체
@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;

    private String name;
    ...
}

 

회원과 팀 객체를 만들고 회원과 팀을 저장하는 샘플 코드를 작성한다.

//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);

//조회
Member findMember = em.find(Member.class, member.getId());

//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());

 

객체는 참조를 사용해 연관된 객체를 찾고, 테이블은 외래키(Foreign Key)로 조인을 사용해 연관된 테이블을 찾는다. 이런 객체와 테이블의 차이가 있기 때문에, 객체를 테이블에 맞추어 테이블 중심으로 모델링하면 협력 관계를 만들 수 없다.

단방향 연관관계

객체의 참조와 테이블의 외래키를 매핑한다.

//회원 객체
@Entity
public class Member {
    ...
    //@Column(name = "TEAM_ID")
    //private Long teamId;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
}

 

회원 객체의 team 속성을 팀 객체로 변경하고, @ManyToOne@JoinColumn 어노테이션을 붙여준다. @ManyToOne은 회원과 팀이 N:1 관계이기 때문에 회원 객체 기준 '다'이기 때문에 사용한다.

//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);

//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);

//조회
Member findMember = em.find(Member.class, member.getId());

//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();

양방향 연관관계

앞에서 회원 객체 -> 팀 객체로 N:1 단방향 연관관계를 설정하였다.

@Entity
 public class Team {
     ...
     @OneToMany(mappedBy = "team")
     List<Member> members = new ArrayList<Member>();
    ...
 }

팀 객체 -> 회원 객체 1:N 단방향 연관관계를 추가로 설정한다. 이렇게 양방향 연관관계가 된다.

연관관계의 주인과 mappedBy

객체와 테이블이 관계를 맺는 방법에는 차이가 있다. 앞의 예시에서 객체 기준 연관관계는 2개로 '회원 객체 -> 팀 객체 단방향 연관관계'와 '팀 객체 -> 회원 객체 단방향 연관관계'가 있고, 테이블 기준 연관관계는 '회원 테이블 <-> 팀 테이블 양방향 연관관계'다. 객체에서도 양방향 연관관계로 얘기하고 있지만, 사실은 서로 다른 단방향 연관관계 2개인 것이다. 객체를 양방향으로 참조하려면 이런식으로 만들어야한다. 하지만 테이블은 외래키 하나로 연관관계를 관리할 수 있다.

 

연관관계에는 주인(Owner)가 필요하다. 객체의 두 관계 중 하나를 연관관계의 주인으로 지정하는데, 주인만 외래키를 관리(등록, 수정)하고 주인이 아닌 쪽은 읽기만 가능하다. mappedBy는 주인이 아닌 쪽이 주인을 지정하기 위해 사용한다. 주인의 선정은 일반적으로 외래키가 있는 곳을 주인으로 한다. 또한 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 것이 좋다(연관관계 편의 메소드 등 사용).

 

단방향 매핑만으로도 이미 연관관계 매핑은 완료된다. 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다. JPQL 사용시에도 역방향으로 탐색을 할 일이 많다. 기본적으로 단방향 매핑을 잘 하고, 양방향 매핑을 필요할 때 추가해도 된다.

 

https://github.com/hongbre/jpa-basic/tree/main

 

GitHub - hongbre/jpa-basic

Contribute to hongbre/jpa-basic development by creating an account on GitHub.

github.com

 

'스터디 > JPA' 카테고리의 다른 글

JPA 스터디2 - 6  (1) 2025.08.27
JPA 스터디2 - 4  (3) 2025.08.03
JPA 스터디2 - 3  (5) 2025.07.30
JPA 스터디2 - 2  (2) 2025.07.14
JPA 스터디2 - 1  (1) 2025.07.13
 

지난 주 토요일 시험으로 AWS 기초 중의 기초인 'Certified Cloud Practitioner(CLF-002)'를 취득하였다. 합격으로 받은 50% 할인 바우처로 'Certified Solutions Architect - Associate'를 취득할 생각이다. Practitioner의 경우 AWS Builders에서 제공하는 에센셜 위주로 듣고, 샘플 시험이나 기타 문서를 참고하였다.

+ Recent posts