이 글에서는 메이븐 프로젝트를 이클립스나 인텔리J에서 임포트하는 방법을 살펴보자.

이클립스에서 메이븐 프로젝트 임포트하기

이클립스에서 [File] -> [Import] 메뉴를 실행한다.

[그림1]

실행한 뒤 Import 대화창에서 Maven/Existing Maven Projects를 선택하고 [Next]를 클릭한다.

[그림2]

[그림2]에서 [Browse] 버튼을 클릭해서 pom.xml 파일이 위치한 폴더를 Root Directory로 선택하고 [Finish] 버튼을 클릭한다. 임포트가 끝나면 다음 그림처럼 이클립스에 프로젝트가 표시된다.

[그림3]

인텔리J에서 이클립스 프로젝트 임포트하기

인텔리J의 [File] -> [Open] 메뉴를 실행한다. Welcome 대화창에서는 Open 메뉴를 실행한다.

[그림4]

Open File or Project 대화창에서 메이븐 프로젝트 폴더를 선택하고 [OK] 버튼을 클릭한다. 잠시후 임포트가 끝나면 다음 그림처럼 프로젝트를 임포트한 결과를 확인할 수 있다.

[그림5]

 

MySQL이나 MariaDB에서 inet_aton/inet_ntoa 함수를 사용하면 문자열로 된 IP 주소를 정수로 저장할 수 있어 저장 용량을 줄이는데 도움이 된다. JPA를 사용하면 AttributeConverter를 사용해서 유사한 변환을 처리할 수 있다.

inet_aton과 inet_ntoa 구현

먼저 다음은 문자열과 정수 간 변환을 처리하는 코드이다.

object Inets {
    const val p3_256 = 256L * 256L * 256L
    const val p2_256 = 256L * 256L

    fun aton(ip: String?): Long? {
        if (ip == null) return null
        val vals: List<Int> = ip.split(".").filter { it.isNotEmpty() }.map { Integer.parseInt(it) }
        if (vals.isEmpty()) return null
        if (vals.size == 1) return vals[0].toLong()
        if (vals.size == 2) return vals[0] * p3_256 + vals[1]
        if (vals.size == 3) return vals[0] * p3_256 + vals[1] * p2_256 + vals[2]
        else return vals[0] * p3_256 + vals[1] * p2_256 + vals[2] * 256L + vals[3]
    }

    fun ntoa(num: Long?): String? {
        if (num == null) return null

        val d = num % 256
        val c = num / 256 % 256
        val b = num / (p2_256) % 256
        val a = num / (p3_256) % 256
        return "$a.$b.$c.$d"
    }
}

MySQL의 inet_aton()에 맞춰 구현했다. 예를 들어 inet_aton()은 "1.1"을 "1.0.0.1"과 동일한 값으로 변환한다. 또 "1.1.1"은 "1.1.0.1"과 같게 변환하고 "1"은 "0.0.0.1"과 같게 변환한다. 이 규칙에 맞게 Inets.aton()을 구현했다.

JPA 컨버터

다음은 Inets를 이용해서 구현한 JPA 컨버터이다.

import javax.persistence.AttributeConverter
import javax.persistence.Converter

@Converter
class InetConverter : AttributeConverter<String, Long> {
    override fun convertToDatabaseColumn(ip: String?): Long? {
        return Inets.aton(ip)
    }

    override fun convertToEntityAttribute(num: Long?): String? {
        return Inets.ntoa(num)
    }

}

사용

다음은 InetConverter를 사용한 코드 예이다.

@Convert(converter = InetConverter::class)
@Column(name = "reg_ip")
val ip: String?

 

다음과 같은 요구사항을 처리할 일이 생겨 분산 락이 필요했다.

  • 어플리케이션에 1분 간격으로 실행하는 작업이 있음
  • 이 작업은 한 쓰레드에서만 실행해야 함
  • 여러 장비에서 어플리케이션을 실행할 수 있음

Zookeeper나 Consul 같은 서비스를 사용하고 있지 않아 DB를 이용해서 단순하게 분산 락을 구현했다.

락 정보 저장 테이블

락 정보를 담을 DB 테이블 구조는 다음과 같이 단순하다.

name은 락 이름, owner는 락 소유자를 구분하기 위한 값, expiry는 락 소유 만료 시간이다.

다음은 MySQL을 위한 테이블 생성 쿼리이다.

CREATE TABLE dist_lock
(
    name   varchar(100) NOT NULL COMMENT '락 이름',
    owner  varchar(100) NOT NULL COMMENT '락 소유자',
    expiry datetime     NOT NULL COMMENT '락 만료 시간',
    primary key (name)
)

 

분산 락 동작

분산 락이 필요한 쓰레드(프로세스)는 다음과 같은 절차에 따라 락을 구한다.

  1. 트랜잭션을 시작한다.
  2. select for update 쿼리를 이용해서 구하고자 하는 행을 점유한다.
  3. owner가 다른데 아직 expiry가 지나지 않았으면 락 점유에 실패한다.
  4. owner가 다른데 expiry가 지났으면 락 owner를 나로 바꾸고 expiry를 점유할 시간에 맞게 변경한다.
  5. owner가 나와 같으면 expiry를 점유할 시간에 맞게 변경한다.
  6. 트랜잭션을 커밋한다.
  7. 락 점유에 성공하면(4, 5) 원하는 기능을 실행한다.
  8. 락 점유에 실패하면(3) 원하는 기능을 실행하지 않는다.

DB 락 구현

실제 락을 구현할 차례다. 원하는 코드 형태는 대략 다음과 같다.

lock.runInLock(락이름, 락지속시간, () -> {
    // 락을 구하면 수행할 작업
});

다음은 DB 테이블을 이용한 분산 락 구현 코드이다.

import javax.sql.DataSource;
import java.sql.*;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.UUID;

public class DbLock {
    private final String ownerId;
    private final DataSource dataSource;

    public DbLock(DataSource dataSource) {
        this.dataSource = dataSource;
        this.ownerId = UUID.randomUUID().toString();
    }

    public void runInLock(String name, Duration duration, Runnable runnable) {
        if (getLock(name, duration)) {
            runnable.run();
        }
    }

    private boolean getLock(String name, Duration duration) {
        Connection conn = null;
        boolean owned;
        try {
            conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            OwnerInfo ownerInfo = getLockOwner(conn, name);
            if (ownerInfo == null) {
                insertLockOwner(conn, name, duration);
                owned = true;
            } else if (ownerInfo.owner.equals(this.ownerId)) {
                updateLockOwner(conn, name, duration);
                owned = true;
            } else if (ownerInfo.expiry.isBefore(LocalDateTime.now())) {
                updateLockOwner(conn, name, duration);
                owned = true;
            } else {
                owned = false;
            }
            conn.commit();
        } catch (Exception e) {
            owned = false;
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException ex) {
                }
            }
        } finally {
            if (conn != null) {
                try {
                    conn.setAutoCommit(false);
                } catch(SQLException ex) {}
                try {
                    conn.close();
                } catch (SQLException e) {
                }
            }
        }
        return owned;
    }

    private OwnerInfo getLockOwner(Connection conn, String name) throws SQLException {
        try (PreparedStatement pstmt = conn.prepareStatement(
                "select * from dist_lock where name = ? for update")) {
            pstmt.setString(1, name);
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new OwnerInfo(
                            rs.getString("owner"),
                            rs.getTimestamp("expiry").toLocalDateTime());
                }
            }
        }
        return null;
    }

    private void insertLockOwner(Connection conn, String name, Duration duration) 
    throws SQLException {
        try(PreparedStatement pstmt = conn.prepareStatement(
                "insert into dist_lock values (?, ?, ?)")) {
            pstmt.setString(1, name);
            pstmt.setString(2, ownerId);
            pstmt.setTimestamp(3, 
                Timestamp.valueOf(
                    LocalDateTime.now().plusSeconds(duration.getSeconds()))
            );
            pstmt.executeUpdate();
        }
    }

    private void updateLockOwner(Connection conn, String name, Duration duration) 
    throws SQLException {
        try(PreparedStatement pstmt = conn.prepareStatement(
                "update dist_lock set owner = ?, expiry = ? where name = ?")) {
            pstmt.setString(1, ownerId);
            pstmt.setTimestamp(2, 
                Timestamp.valueOf(
                    LocalDateTime.now().plusSeconds(duration.getSeconds()))
            );
            pstmt.setString(3, name);
            pstmt.executeUpdate();
        }
    }
}

https://github.com/madvirus/db-lock-sample 에서 코드를 확인할 수 있다.

추가 고려사항

현재 구현은 당장의 요구를 충족하는데 필요한 만큼만 기능을 구현한 것으로 다음을 고려한 개선이 필요하다.

  • runInLock()에서 실행하는 코드의 실행 시간이 락 지속 시간보다 길면 안 됨
  • 명시적으로 락을 해제하는 기능 없음

 

+ Recent posts