Problem


삼각형의 꼭대기부터 가장 아래로 가는 최단 거리를 구하는 문제입니다.

위에서 아래로 내려갈 때 이동할 수 있는 곳은 인접한 대각선 밖에 없습니다.



Solution

현재까지 이동한 거리를 누적해서 더해나가다가 마지막에 최소값을 구하면 됩니다.

사실 이 문제는 위에서 아래로 내려가는 것보다 아래에서 위로 올라가는 Bottom Up 방식이 더 쉽게 구현할 수 있습니다.

우선 누적합을 저장해두는 accSum 배열을 선언합니다.

그리고 아래층부터 step 을 따라 올라가며 이동할 수 있는 거리를 구합니다.

양쪽 모두에서 이동할 수 있으므로 두 숫자 중 최솟값과 현재 step 값을 더하는 방식으로 쭉쭉 올라가면 됩니다.

image



Java Code

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        // init 0
        int[] accSum = new int[triangle.size() + 1];

        // bottom up
        for (int i = triangle.size() - 1; i >= 0; i--) {
            List<Integer> step = triangle.get(i);

            for (int j = 0; j < step.size(); j++) {
                int min = Math.min(accSum[j], accSum[j + 1]);
                accSum[j] = min + step.get(j);
            }
        }

        return accSum[0];
    }
}

Overview

과거 포스트에서 이미 MacOS OpenJDK 설치 및 버전 관리에 대해 다뤄본 적이 있으나 asdf 를 이용해서 설치하는 방법을 안내하려고 합니다.

원래 Java 를 설치하려면 brew 를 사용하거나 직접 홈페이지에 들어가 JDK 파일을 다운받아야 합니다.

하지만 한 PC 에서 여러 Java 버전을 사용한다면 터미널에서 빌드할 때마다 Java 버전을 바꿔야하고 관리하기도 까다롭습니다.

jenv 라는 Java 버전 관리 툴이 존재하지만 jenv 는 Java 를 직접 설치할 수는 없습니다.

하지만 asdf 라는 툴을 사용하면 Java 의 설치/삭제를 간단하게 하고 버전 관리도 편하게 할 수 있습니다.

뿐만 아니라 asdf 는 Java 외의 여러 언어, 오픈소스 등의 버전도 관리할 수 있습니다.


1. asdf 설치

mysetting - asdf 을 참고하면 설치 및 사용방법 등을 알 수 있습니다.

# install dependencies (필요시)
$ brew install coreutils curl git

# install asdf
$ brew install asdf

# add to shell
$ echo -e "\n. $(brew --prefix asdf)/asdf.sh" >> ~/.zshrc

우선 asdf 를 설치합니다.

마지막의 add to shell 은 사용자마다 다릅니다.

저는 zsh 를 사용하고 있기 때문에 ~/.zshrc 에 추가했고 만약 bash 를 사용한다면 ~/.bash_profile 에 추가하면 됩니다.


2. Java Plugin 설치

$ asdf plugin add java
$ asdf plugin update java

설치를 위해선 플러그인을 먼저 설치해야 합니다.


3. Java 버전 리스트 확인

$ asdf list-all java
adoptopenjdk-11.0.15+10
adoptopenjdk-11.0.16+8
adoptopenjdk-11.0.16+101
adoptopenjdk-11.0.17+8
adoptopenjdk-17.0.0+35
...
..
.
zulu-jre-javafx-19.30.11

설치할 수 있는 Java 버전을 확인합니다.

저는 원래 AdoptOpenJDK 를 사용하였으나 deprecated 되었기 때문에 Adoptimu 에서 권장하는 Temurin 버전을 사용합니다.

(AdoptOpenJDK Blog - Good-bye AdoptOpenJDK. Hello Adoptium! 참고)


4. Java 설치

# 설치
$ asdf install java temurin-11.0.17+8

# 설치된 확인
$ asdf list java
  temurin-11.0.17+8

Temurin 의 Java 11 버전 중 가장 최신 버전을 설치합니다.

설치 후에는 asdf list <언어> 명령어로 설치된 버전을 확인할 수 있으며 asdf list 만 입력하면 설치된 모든 오픈 소스의 모든 버전을 볼 수 있습니다.


5. 사용할 버전 지정

# global
$ asdf global java temurin-11.0.17+8

# local
$ asdf local java temurin-11.0.17+8

프로젝트 별로 설정하고 싶다면 local, 전역으로 설정하고 싶다면 global 을 사용해 지정합니다.


6. JAVA_HOME 설정하기

$ . ~/.asdf/plugins/java/set-java-home.zsh

halcyon/asdf-java - JAVA_HOME 를 참고해서 본인이 쓰는 shell 에 맞게 입력합니다.


7. Java 설치 완료

$ java -version
openjdk version "11.0.17" 2022-10-18
OpenJDK Runtime Environment Temurin-11.0.17+8 (build 11.0.17+8)
OpenJDK 64-Bit Server VM Temurin-11.0.17+8 (build 11.0.17+8, mixed mode)

터미널에서 자바 버전을 확인해서 제대로 나온다면 설치 완료입니다.


Reference

Overview

Ruby 언어에 관한 정보를 정리합니다.

나중에 더 추가될 수도 있습니다.


1. Variables

number = 1
puts number # => 1

large_number = 2

변수 이름이 길어질 때는 snake_case 를 사용합니다.


2. Data Types

Ruby 에는 다음과 같은 타입들이 있습니다.

기본적으로 최상위 타입은 전부 Object 입니다.

  • Numbers
  • Strings (texts)
  • True, False, and Nil
  • Symbols
  • Arrays
  • Hashes

2.1. Numbers

a = 1   # Integer
b = 1.2 # Float

c = 1_000_000 # == 1,000,000

숫자 타입입니다.

Integer, Float, BigDecimal 등등이 존재합니다.


2.2. Strings

"hi" + "hi"         # => "hihi"
"hi" * 3            # => "hihihi"

"hello".upcase      # => "HELLO"
"hello".capitalize  # => "Hello"
"hello".length      # => 5
"hello".reverse     # => "olleh"

문자열입니다.

큰 따옴표가 아니라 작은 따옴표로 묶어서 사용할 수도 있습니다.


2.3. Symbols

Symbol 은 문자열과 비슷하지만 조금 다릅니다.

앞에 콜론이 붙어있으면 Symbol 입니다. (:something)

Symbol 은 보통 텍스트를 데이터 가 아닌 코드 로 사용할 때 많이 사용합니다.

예를 들면 Hash Key 같은 경우 Key 로 사용한다는 것에 의미가 있죠.

그리고 String 과 비교했을 때 가장 두드러지는 부분은 같은 Symbol 은 동일한 객체를 참조한다는 점입니다.

String 은 매번 다른 객체를 생성하지만 Symbol 은 동일한 객체를 재사용합니다.

"string".object_id # => 2409957680
"string".object_id # => 2682952200
"string".object_id # => 2409974840

:symbol.object_id # => 881948
:symbol.object_id # => 881948
:symbol.object_id # => 881948

2.4. Array

a = [1, 2, 3]

a[0]        # 1
a << 4      # a == [1, 2, 3, 4]
a[6] = 7    # a == [1, 2, 3, 4, nil, nil, 7]
a.first     # 1
a.last      # 7
a.length    # 7
a.compact   # [1, 2, 3, 4, 7]

a.compact 처럼 새로운 배열을 뽑을 때 현재 배열 자체를 바꾸고 싶다면 마지막에 ! 를 붙여주면 됩니다. (a.compact!)


2.5. Hash

dictionary = { "one" => "eins", "two" => "zwei", "three" => "drei" }
dictionary["one"]               # "eins"
dictionary["zero"] = "null"     # insert

Key, Value 로 지정할 수 있는 타입에는 제한이 없습니다.

위 예시에서는 둘다 String 이었지만 Key 로 Integer 를 사용해도 되고 Value 에 배열이 들어가도 됩니다.

가장 많이 사용되는 Key 타입은 Symbol 입니다.

Symbol Key Hash 는 좀더 간편하게 표현할 수 있게 지원해줍니다.

아래 두 문법은 동일한 기능을 갖습니다.

a = { one: "eins", two: "zwei", three: "drei" }
a = { :one => "eins", :two => "zwei", :three => "drei" }

a[:one] # => "eins"

2. String interpolation

str = "Ruby"
puts "Hello, #{str}"
puts "1 + 2 = #{1 + 2}"

#{} 로 감싸면 다른 변수를 넣을 수 있습니다.


3. Method

def add(a, b)
  a + b
end

add(1, 2) # => 3
add 1, 2  # => 3

Ruby 의 메서드는 return 이 없어도 맨 마지막 값을 자동으로 리턴합니다.

메서드를 호출할 때는 괄호로 감싸지 않아도 호출 가능합니다.


3.1. 괄호 생략

def print
  puts "Hello, World!"
end

print() # => Hello, World!
print # => Hello, World!

메서드를 정의할 때 파라미터가 필요 없다면 생략할 수 있습니다.

호출 역시 마찬가지로 생략 가능합니다.


3.2. Default parameter

def print(greeting = "Hello", target = "World")
  puts "#{greeting} #{target}"
end

print               # Hello World
print("Hi")         # Hi World
print("Hi", "Ruby") # Hi Ruby

파라미터 값이 없을 때의 기본값을 넣어줄 수 있습니다.


4. Class

class Person
end

클래스는 위와 같이 정의할 수 있습니다.

person = Person.new 처럼 객체를 생성할 수 있습니다.


4.1. Ruby Class 의 인스턴스, 클래스란?

Ruby 에서는 인스턴스 변수, 인스턴스 메서드, 클래스 변수, 클래스 메서드 등 인스턴스와 클래스라는 단어가 많이 나옵니다.

Ruby 에서 말하는 인스턴스, 클래스는 다음과 같습니다.

  • 인스턴스
    • 객체를 생성 (new) 해야 사용할 수 있는 변수, 메서드
    • 인스턴스 변수는 객체끼리 독립된 값을 가짐
  • 클래스
    • 객체 생성 없이 클래스 자체로 생성할 수 있는 변수, 메서드
    • 클래스 변수는 같은 클래스로 생성된 여러 객체가 공유함

4.2. 인스턴스 변수

class Person
  def print_name
    puts "My Name is #{@name}"
  end
end

p1 = Person.new
p1.print_name   # My Name is
p1.name = "woody"
p1.print_name   # My Name is woody

인스턴스 변수는 클래스 내에서 @ 가 붙어있는 변수를 의미합니다.

일회성으로 사용되고 끝나는 변수와 달리 생성된 인스턴스 내에서 계속 사용할 수 있습니다.


4.3. 클래스 변수

class Person
  @@name = "default"

  def name
    @@name
  end

  def name=(name)
    @@name = name
  end
end

p1 = Person.new
p2 = Person.new
p1.name     # default
p2.name     # default

p1.name = "woody"
p1.name     # woody
p2.name     # woody

클래스 변수는 모든 객체가 공유하는 변수입니다.

위 코드에서 볼수있듯이 p1 의 변수를 바꾸었지만 p2 의 변수도 똑같이 바뀐 것을 볼 수 있습니다.

이것이 인스턴스 변수와의 차이점입니다.


4.4. 상수

class Person
  AGE = 24
end

Person::AGE # 24

상수는 변하지 않는 변수입니다.

객체를 직접 생성하지 않아도 바로 사용할 수 있습니다.


4.5. 인스턴스 메서드, 클래스 메서드

class Sample
  def print
    puts "Instance Method"
  end

  def self.print
    puts "Class Method"
  end
end

Sample.new.print # Instance Method
Sample.print     # Class Method

인스턴스 메서드는 객체를 생성한 뒤에 사용하는 메서드, 클래스 메서드는 그대로 사용하는 메서드입니다.

인스턴스 메서드는 def 처럼 평범하게 정의하면 되지만 클래스 메서드는 def self 를 사용해서 정의해야 합니다.


4.6. 생성자

class Person
  def initialize(name)
    @name = name
  end
end

Person.new("woody") # => #<Person:0x000000012cebfeb0 @name="woody">
Person.new          # ArgumentError (wrong number of arguments (given 0, expected 1))

Ruby 에서는 initialize 메서드로 생성자를 선언할 수 있습니다.

생성자를 선언하지 않으면 파라미터가 없는 기본 생성자를 사용할 수 있습니다.

인스턴스 변수를 초기화할 때는 일반적으로 생성자를 사용합니다.


4.7. Accessor (Getter, Setter)

class Person
  attr_accessor :name
  attr_reader :age
  attr_writer :address
end

p = Person.new

# accessor (getter, setter)
p.name # => nil
p.name = "adsf"
p.name # => "asdf"

# reader (getter)
p.age # => nil
p.age # NoMethodError (undefined method `age=' for #<Person:0x000000012cb9ce90 @name="asdf">)

# writer (setter)
p.address # NoMethodError (undefined method `address' for #<Person:0x000000012cb9ce90 @name="asdf">)
p.address = "Seoul"

attr_accessor, attr_reader, attr_writer 를 사용해서 Getter, Setter 를 생성해줄 수 있습니다.

반대로 private 을 사용해서 Getter, Setter 를 숨길 수도 있습니다.

Accessor 로 지정한 변수는 인스턴스 변수로 사용 가능합니다.


4.8. 상속

class Fruit
  attr_accessor :name

  def print
    puts "This is Fruit"
  end

  def super_method
    puts "This is super class method"
  end
end

class Apple < Fruit
  def print
    puts "This is Apple"
  end
end

< 키워드를 사용해서 상속을 표현할 수 있습니다.

상속받은 클래스에서는 상위 클래스의 메서드나 변수를 사용할 수 있으며 오버라이드도 가능합니다.


4.9. 클래스 확장

클래스 확장이란 기존에 정의된 클래스에 새로운 메서드나 변수를 추가하는 것을 의미합니다.

새로운 기능을 추가하는 방법은 총 세가지지만 기존에 정의된 메서드를 중복 정의하는 경우 새롭게 덮어 씌운다는 공통점이 있습니다.

우선 Game 이라는 기본적인 클래스를 정의합니다.

class Game
  def initialize
    @name = "lol"
  end
end

1. << 키워드 사용

# 인스턴스
game = Game.new

class << game
  attr_accessor :age

  def print_one
    puts "print_one: #{@name}"
  end
end

# 클래스
class << Game
  def print_class
    puts "print_class"
  end
end

<< 키워드를 사용하면 동적으로 새로운 변수, 메서드를 추가할 수 있습니다.


2. 확장 함수로 정의

# 인스턴스
def game.print_two
  puts "print_two: #{@name}"
end

# 클래스
def Game.print_class
  puts "print_class"
end

Kotlin 의 확장 함수와 비슷한 문법입니다.


3. 클래스 분할 정의

class Game
  def print_three
    puts "print_three: #{@name}"
  end

  def self.print_class
    puts "print_class"
  end
end

Ruby 는 클래스를 여러 번 정의할 수 있습니다.

분할 정의된 클래스의 내용은 기존 클래스에 그대로 추가됩니다.


5. Module

모듈 (Module) 이란 여러 가지 기능을 모은 것을 의미합니다.

모듈은 크게 두 가지 목적으로 사용합니다.

  • Namespace 구분
    • 중복된 이름의 클래스를 사용할 때 충돌이 발생하지 않도록 합니다
    • V1, V2 처럼 버전 구분을 할 때 유용
  • 중복된 기능 모으기
    • 여러 곳에서 사용하는 중복된 기능을 분리해서 각각의 모듈에 담을 수 있습니다

5.1. Definition

module Sample
end

모듈은 위와 같이 정의할 수 있습니다.


5.2. Namespace

module V1
  class API
    def self.call
      puts "Call API v1"
    end
  end
end

module V2
  class API
    def self.call
      puts "Call API v2"
    end
  end
end

V1::API.call # Call API v1
V2::API.call # Call API v2

원래 동일한 이름의 클래스와 메서드를 선언하면 분할 정의로 취급하여 기존 메서드는 사라졌습니다.

하지만 모듈을 사용해 분리했기 때문에 두 클래스는 같은 이름과 같은 메서드를 같지만 다른 클래스, 메서드처럼 동작합니다.


5.3. Module Function

module Person
  ADDRESS = "Seoul"

  def age
    @age
  end

  def age=(age)
    @age = age
  end

  def company
    "company"
  end

  module_function :age, :age=
end

Person::ADDRESS # => "Seoul"
Person.age      # => nil
Person.age = 24
Person.age      # => 24
Person.company  # NoMethodError (undefined method `company' for Person:Module)

모듈에서 변수나 메서드를 정의할 수 있습니다.

모듈은 객체처럼 생성이 불가능하기 때문에 사실상 모든 변수와 메서드는 클래스 변수, 메서드라고 볼 수 있습니다.

정의된 메서드를 모듈에서 직접 사용하기 위해선 module_function 키워드를 사용해서 지정해줘야 합니다.


5.4. Mixin

Module 을 Class 에서 참조하면 마치 클래스의 메서드인것처럼 사용할 수 있습니다.

이걸 mix-in 이라고 부릅니다.

모듈을 mixin 하는 방법에는 include, prepend, extend 총 세가지가 있습니다.


5.4.1. Include

module IncludeModule
  def hello
    puts "Hello, Include"
  end
end

class Person
  include IncludeModule

  def hello
    puts "Hello, Person"
  end
end

person = Person.new
person.hello    # Hello, Person

include 키워드를 사용해서 모듈을 참조하면 모듈의 메서드를 인스턴스 메서드로 사용할 수 있습니다.

만약 동일한 이름의 메서드가 모듈과 클래스 양쪽에 정의되어 있다면 클래스의 메서드를 우선시합니다.


5.4.2. Prepend

module PrependModule
  def hello
    puts "Hello, Prepend"
  end
end

class Person
  prepend PrependModule

  def hello
    puts "Hello, Person"
  end
end

person = Person.new
person.hello    # Hello, Prepend

prependinclude 와 마찬가지로 모듈의 메서드를 인스턴스 메서드로 사용할 수 있습니다.

하지만 include 와 다르게 동일한 이름의 메서드가 정의되어 있을 경우 모듈의 메서드를 우선시합니다.


5.4.3. Extend

module ExtendModule
  def hello
    puts "Hello, Extend"
  end
end

class Person
  extend ExtendModule
end

Person.hello    # Hello, Extend

extend 는 다른 mixin 과 다르게 클래스 메서드로 사용 가능합니다.

중복 정의된 메서드는 include 와 마찬가지로 클래스에 있는 걸 우선시합니다.


5.4.4. Ancestors

만약 여러 개의 모듈을 한번에 include 하면 어떻게 될까?

include, prepend 가 여러개 섞여 있으면 어떻게 될까?

Ruby 에서는 어떤 오브젝트가 젤 우선시 되는지 ancestors 메서드로 확인할 수 있습니다.


module Include1
end

module Include2
end

module Prepend1
end

module Prepend2
end

class Human
end

class Person < Human
  include Include1
  include Include2
  prepend Prepend1
  prepend Prepend2
end

Person 클래스에서는 여러 모듈을 동시에 mixin 하고 있습니다.

prepend -> Person -> include 순서까지는 쉽게 짐작할 수 있지만 같은 키워드를 사용한 모듈들은 어떤게 우선시 되는지 알 수 없습니다.

게다가 상위 클래스에도 중복된 메서드가 정의되어 있다면 더욱 더 헷갈립니다.

이를 확인할 수 있는게 ancestors 메서드입니다.


Person.ancestors
=> [Prepend2, Prepend1, Person, Include2, Include1, Human, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, ActiveSupport::Tryable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]

Person.ancestors 를 사용하면 해당 클래스가 참조하고 있는 모든 오브젝트가 나옵니다.

그리고 중복 정의된 메서드가 있으면 앞 순서에 있는 오브젝트의 메서드가 우선시됩니다.

그래서 Person 클래스에서 중복된 메서드가 있으면 Prepend2 모듈의 메서드가 사용 됩니다.

모든 클래스는 오브젝트라서 BasicObject 가 최상단에 있는 걸 알 수 있습니다.


Reference

'Framework > RubyOnRails' 카테고리의 다른 글

RubyOnRails 세션  (0) 2021.11.06
Ruby 의 as_json 과 to_json 의 차이  (0) 2021.06.06
RSpec Test Frameworks  (0) 2020.07.14
RubyOnRails - nil? empty? blank? present? 차이점  (0) 2020.07.14
Ruby Regular Expression (정규 표현식)  (0) 2020.07.14

Issue

Ruby Grape 와 Grape Entity 를 사용할 때 클래스를 참조하지 못하고 자꾸 NameError (uninitialized constant {Class}) 에러가 발생했습니다.

처음에는 Rails 버전과 Grape 버전이 호환되지 않아서 발생하는 이슈인가 해서 다운그레이드도 해봤지만 해결되지 않았습니다.


Solution

제가 이런 오류를 겪게된 이유는 크게 두가지였는데요.

첫번째는 제가 Ruby 에 익숙하지 않아서 발생한 일이고 다른 하나는 Grape 문서를 제대로 읽지 않아서 발생했습니다.


1. Ruby File 형식을 준수하고 파일 이름과 Class 이름이 일치해야 함

SimpleResponseEntity 라는 클래스를 사용한다고 하면 파일 이름을 simple_response_entity.rb 로 만들어야 합니다.

무심코 Java 에서의 버릇처럼 Camel Case 로 작성했던게 문제였습니다.


2. Grape 프레임워크는 Module 과 파일의 경로가 일치해야 함

Ruby Grape Docs > Mounting > Rails 파트를 보면 다음과 같은 문장이 있습니다.

Place API files into app/api. Rails expects a subdirectory that matches the name of the Ruby module and a file name that matches the name of the class. In our example, the file name location and directory for Twitter::API should be app/api/twitter/api.rb.


즉, Grape 관련된 기능을 사용하기 위해선 파일을 app/api 하위에 만들어야 하며 subdirectory 와 module 이름까지 일치해야 한다는 뜻입니다.

Rails 자체도 파일이나 경로를 굉장히 중요시하는 것처럼 Grape 도 비슷한 특징을 갖고 있는 것 같습니다.


Reference

Overview

MySQL 테이블을 설계할 때 보통 자주 사용하는 조건 컬럼에는 Index 를 추가합니다.

다양한 쿼리를 사용하는 경우 인덱스를 여러 개 추가하는데, 인덱스의 컬럼이 겹치면 원하지 않는 인덱스를 타서 Slow Query 가 발생할 수 있습니다.

Slow Query 의 발생가능한 원인과 해결 방법에 대해 간단히 알아봅니다.


1. Optimizer

SQL 쿼리를 호출하면 DBMS 에 존재하는 Optimizer 가 SQL 실행계획을 만들어줍니다.

옵티마이저는 두 가지 종류로 나뉩니다.

  • RBO (Rule Based Optimizer)
    • 미리 정해진 규칙대로만 쿼리를 수행
    • 인덱스가 존재하면 무조건 인덱스를 탐
    • 예측 가능하기 때문에 설계하기 쉽지만 그만큼 쿼리 자체를 잘 짜야함
  • CBO (Cost Based Optimizer)
    • 통계 정보 (Record 수, Index 컬럼 값 갯수) 를 기반으로 옵티마이저가 생각하는 가장 효율적인 쿼리를 수행
    • 인덱스가 존재해도 Table Full Scan 이 더 효율적이라고 생각하면 Full Scan 함
    • 예측이 힘들기 때문에 우리가 생각한 실행계획과 달라 Slow Query 가 발생할 수 있음
      • 특히 로컬, 개발 환경과 실제 운영 환경에서는 데이터의 갯수가 달라 같은 쿼리여도 다르게 수행될 수 있음

최근 대부분의 RDBMS 는 CBO 를 사용한다고 합니다.

예를 들어 우리가 WHERE ~ AND ~ 조건을 사용할 때 Index 컬럼 순서를 지키지 않았지만 자동으로 인덱스를 태우는 것도 다 CBO 덕분입니다.

하지만 CBO 를 사용하는 경우 우리 생각과는 다르게 동작할 수 있습니다.

여러 인덱스가 존재할 때 A 인덱스를 타는게 더 효율적임에도 B 인덱스를 타는 경우가 있습니다.

이런 경우에 사용하는 것이 바로 Index Hint 입니다. (쿼리 최적화의 또다른 방법으로 Optimizer Hint 라는 것도 있는데 이번 포스팅에서는 다루지 않습니다)


2. Index Hint

인덱스 힌트는 이름 그대로 옵티마이저가 인덱스를 선택할 때 도움을 줍니다.

특정 인덱스를 사용하지 않길 원하면 IGNORE, 사용하길 원하면 USE, FORCE 를 사용할 수 있습니다.

그렇다면 USEFORCE 의 차이는 뭘까요?

Stackoverflow - MySQL : FORCE INDEX vs USE INDEX 를 참고해보면 둘은 다음과 같은 차이가 있습니다.

  • USE INDEX
    • DB 옵티마이저에게 지정한 인덱스를 사용하라고 권장
    • 하지만 만약 Table Scan 이 더 빠르다면 옵티마이저는 인덱스 대신 Table Scan 수행 가능
  • FORCE INDEX
    • Table Sacan 이 더 효율적이어도 무조건 인덱스 사용
    • Index 를 사용할 수 없는 쿼리 (인덱스가 걸려있지 않은 컬럼이 조건) 인 경우에만 다른 방법 선택 가능

대부분의 경우에는 USE INDEX 만 사용해도 되지만 Full Scan 도 허용하지 않는 경우에는 FORCE INDEX 를 사용하면 될 것 같습니다.


Reference

'공부 > Database' 카테고리의 다른 글

MySQL Index 특징 및 유의사항 정리  (2) 2022.05.22
Cache 전략  (0) 2022.05.20
Redis 설치 및 명령어  (0) 2021.08.07

Overview

한 프로젝트를 여러 사람이 작업하면 각자의 feature 브랜치에서 수정한 내용을 하나의 통합 브랜치 (main) 에 합치는 방식으로 진행합니다.

이렇게 브랜치를 통합할 때 사용하는 명령어가 git merge 입니다.

이 merge 는 각 브랜치의 상황에 따라 다르게 동작하고 방법도 다양하기 때문에 어떤 방법들이 있는지 알아봅니다.


1. Git Merge

CLI 또는 GUI 에서 사용하는 경우입니다.

크게 Merge, Fast-Forward, Squash, Rebase 가 있습니다.


1.1. Merge

$ git switch main
$ git merge feature

main 브랜치에 추가 작업 내역이 있다면 새로운 Merge Commit 을 만들게 됩니다.

가장 일반적인 Merge 방법입니다.

feature 의 모든 커밋 로그와 하나로 합친 Merge Commit 로그가 전부 남습니다.


1.2. Fast-Forward

$ git switch main
$ git merge feature # main 에 추가 작업 내역 없음

feature 브랜치를 딴 이후로 main 브랜치에 아무런 커밋이 없다면 merge 할 때 Fast-Forward 방식으로 합쳐집니다.

Fast-Forward 를 그대로 직역하면 "빨리감기" 라는 뜻입니다.

이 말 그대로 별도의 Merge 기록 없이 원래 main 에서 작업한 것처럼 로그가 남습니다.

병합하려는 main 브랜치에 커밋이 존재한다면 Fast-Forward Merge 가 되지 않습니다.


1.3. No Fast-Forward (--no-ff)

$ git switch main
$ git merge --no-ff feature # main 에 추가 작업 내역 없지만 머지 커밋 생성

만약 main 에 추가 작업 내역이 없어도 새로운 Merge Commit 을 만들고 싶다면 --no-ff 옵션을 추가합니다.

feature 브랜치의 존재를 남기고 싶을 때 사용할 수 있습니다.


1.4. Squash (--squash)

$ git switch main
$ git merge --squash feature
$ git commit -m "Merge Squash feature/squash"

Squash Merge 는 feature 의 모든 커밋을 하나의 커밋으로 만들어 main 에 머지합니다.

feature 에서 리뷰 반영, 버그 수정 등으로 쓸데없는 커밋이 많아진 경우 이를 다 기록하지 않고 하나의 새로운 커밋으로 남길 수 있습니다.

대신 feature 브랜치의 수정사항이 큰 경우 하나의 커밋으로 전부 표현하기 보다 커밋을 잘개 쪼개는게 알아보기 더 편할 수 있기 때문에 신중히 사용해야 합니다.


1.5. Rebase

$ git switch main
$ git rebase feature

main 에 아무런 추가 커밋이 없다면 Fast-Forward 와 동일하게 HEAD 만 이동합니다.

하지만 다른 커밋이 있다면 이름 그대로 커밋을 재배치 합니다.

재배치 하고 나면 현재 브랜치의 커밋이 rebase 하려는 브랜치의 뒤로 이동합니다.

main 브랜치에서 git rebase feature 를 했다면 feature 의 커밋 내역이 먼저 찍히고 이후 main 브랜치의 커밋이 찍힙니다.

만약 같은 범위를 수정해서 rebase 과정에서 충돌이 발생한다면 각 커밋 별로 충돌을 해결해야 합니다.

별도의 Merge Commit 이 남지 않는다는 점은 Fast-Forward 와 동일하지만 Rebase 는 각 브랜치에 다른 커밋이 있어도 하나의 줄기로 합쳐줄 수 있다는 장점이 있습니다.


위 사진과 같이 어디 브랜치에서 시작하냐에 따라 커밋의 순서가 바뀝니다.

현재 checkout 한 브랜치의 커밋이 가장 최신에 위치하는 걸 볼 수 있습니다.


2. Github Merge

Github 에서도 Merge 를 할 수 있습니다.

아마 Github 에서 Pull Request 로 코드 리뷰를 받은 후에 Merge 하는 경우가 더 많을 것 같습니다.

Github 에서 지원하는 Merge 는 크게 세종류입니다.


2.1. Create a merge commit

Create a merge commit 을 사용하면 main 브랜치에 커밋이 있건말건 무조건 --no-ff 옵션으로 머지됩니다.

커밋 로그가 전부 남으며 새로운 Merge Commit 이 함께 만들어집니다.


2.2. Squash and merge

Git 과 마찬가지로 feature 의 커밋 이력들 대신 새로운 Merge Commit 하나만 남깁니다.

저는 리뷰 수정사항이 많아서 기능에 비해 커밋 수가 너무 많을 때 사용하는 방법입니다.


2.3. Rebase and merge

Rebase 는 feature 의 커밋 로그를 main 브랜치 커밋 로그 뒤에 붙여줍니다.

위에서 설명할 때 rebase 를 하면 현재 브랜치의 커밋이 대상 브랜치의 커밋 뒤로 이동한다고 했습니다.

이 특성을 이용해서 우선 feature 브랜치에서 main 브랜치를 rebase 한 뒤, feature -> main 으로 Fast-Forward Merge 한 셈입니다.

Git 명령어로 나타내면 아래와 같습니다.


# feature 브랜치로 이동
$ git switch feature

# main 브랜치의 커밋내역을 feature 브랜치 이전으로 끼워넣기
# 이렇게 하면 최신 main 브랜치에서 feature 를 딴 뒤 수정한 것처럼 됨
$ git rebase main 

# 다시 main 브랜치로 이동
$ git switch main

# main 에 feature 브랜치 머지
# rebase 를 했기 때문에 HEAD 만 이동하는 fast-forward merge 실행
$ git merge feature

이렇게 하면 깔끔하게 main 브랜치 뒤에 feature 브랜치 커밋 로그를 남길 수 있습니다.

다만, feature 브랜치의 커밋이 엄청 많은 경우 전부 rebase merge 해버리면 트리만 봤을 때 작업의 큰 줄기가 잘 보이지 않는다는 단점이 있습니다.


Conclusion

그래서 어떤걸 사용하는게 좋냐? 라고 한다면 정해진 답은 없습니다.

여러 개의 커밋을 남기고 싶다면 --no-ff 머지를 사용하고 하나만 깔끔하게 남긴다면 Squash 또는 Rebase 를 사용합니다.

현재 제가 사용하는 방식을 그림으로 나타내면 아래와 같습니다.


저는 대부분의 머지를 PR 에서 하기 때문에 PR 을 최대한 작은 단위로 나눈 후 전부 Squash Merge 하는 걸 선호했습니다.

코드리뷰를 받다보면 추가 수정 사항이 발생하고 자잘한 커밋들을 전부 남기면 지저분하게 느껴서 없애고 싶었어요.

회사에 와서 git rebase main 으로 feature 브랜치를 최신화 하고 --no-ff 옵션으로 Merge Commit 을 남기는 방법을 알게 되었는데 굉장히 깔끔한 것 같습니다.

여러 회사들의 기술 블로그를 보면 각 팀마다 사용하는 Merge 전략이 있는데 여러 가지를 찾아보고 본인이 느끼기에 가장 좋아보이는 전략을 선택하면 됩니다.

'공부 > Git' 카테고리의 다른 글

Gitmoji (for Git Commit Convention)  (0) 2022.06.18
Git Directory 이름 변경  (0) 2020.04.26

Overview

Gitmoji 란 Git + Emoji 입니다.

여러 사람이 커밋 메시지를 작성하다보면 일관성이 없고 나중에는 히스토리를 알아보기 힘들어집니다.

Gitmoji 는 이모지를 사용하여 커밋 메시지를 일정하게 작성하도록 도와주는 툴입니다.

커밋 메시지 타이틀 앞에 특정 이모지를 넣으면서 이모지만 보고도 어떤 목적으로 한 커밋인지 알아볼 수 있습니다.

gitmoji 에 대한 설명은 gitmoji 공식에서 볼 수 있지만 사용하기 위해서는 gitmoji-cli 를 참고해야 합니다.


1. Install

# use brew
$ brew install gitmoji

# use npm
$ npm i -g gitmoji-cli

brew 또는 npm 을 사용해서 설치할 수 있습니다.


2. Configuration

gitmoji 를 사용하기 전에 미리 설정을 해두는게 좋습니다.

gitmoji -g 명령어를 사용해서 여러가지 옵션을 설정할 수 있습니다.

저는 대부분 기본값을 사용하는데 Select how emojis should be used in commits 옵션만 text 대신 emoji 를 선택했습니다.


Github 에서 볼 때는 별 문제 없으나 이렇게 이모지를 지원하지 않는 외부 툴에서는 Text 그대로 노출됩니다.

그래서 이모지 자체를 커밋 메시지에 넣을 수 있도록 설정했습니다.


3. Usage

gitmoji 의 사용법은 크게 어렵지 않습니다.

그냥 평범한 사용법에서 git commit 대신 gitmoji -c 를 입력해주면 됩니다.


3.1. Choose Gitmoji

gitmoji -c 를 입력하면 위 그림처럼 이모지 선택이 나옵니다.

위아래 방향키를 사용해서 다른 이모지들을 찾아볼 수도 있고, 직접 원하는 기능을 검색할 수도 있습니다.


위 그림처럼 refactor 를 검색하면 그에 맞는 이모지를 띄워줍니다.


3.2. Input Commit Message

원하는 이모지를 선택했다면 Commit Title, Message 를 입력합니다.

Message 는 생략해도 됩니다.

전부 입력 후 엔터를 누르면 커밋이 완료됩니다.

이후에는 똑같이 git push 를 사용해서 원격 저장소에 반영할 수 있습니다.


3.3. Repository 확인

이제 커밋 로그를 확인하면 이렇게 이모지가 잘 들어간 것을 볼 수 있습니다.

제가 실제로 두번 커밋한거고 중복해서 로그가 쌓인건 아닙니다.


4. Plugin 및 GUI

IntelliJ 는 Plugin, VSCode 는 Extension 으로 지원하기도 합니다.

IDE 에서 직접 커밋 메시지를 작성하는 스타일이었다면 이용해보는 게 좋습니다.

Source Tree, GitKraken, Git Fork 등등 별도 GUI 를 사용할 때는 아쉽게도 지원되지 않는 것 같아요.

개인적으로 CLI 대신 GUI 를 많이 사용하기 때문에 이 부분이 참 아쉬웠습니다.

그래서 그냥 add, push, pull 등은 전부 GUI 를 이용하고 오로지 커밋만 gitmoji -c 를 사용합니다.

만약 이게 싫다면 이모지를 직접 복사해서 GUI 커밋 메시지에 붙여넣는 방법도 가능합니다.

Gitmoji Dev 사이트에서 원하는 이모지를 검색할 수 있고 그림을 누르면 자동으로 복사되기 때문에 GUI 에서 작성하는 경우 편리하게 이용할 수 있습니다.


5. 장단점

가장 큰 장점은 커밋 로그를 시각적으로 확인할 수 있다 입니다.

단점은 이모지가 뭘 의미하는지 알고 있어야 하고 GUI 와 같이 쓰기 번거롭다는 점인것 같아요.

Gitmoji 를 처음 봤을 때 생각난건 feat:, fix:, refactor: 등을 사용하는 Angular Git Commit Guidelines 였는데요.

시각적으로는 그림인 이모지가 더 잘들어오긴 하지만 가독성은 개인에 따라 다르기 때문에 뭐가 더 낫다고 단언할 수는 없을 것 같아요.

Angular Convention 과 비교했을 때 가장 큰 장점을 뽑자면 Search 기능이라고 생각합니다.

파일을 수정하고 어떤 prefix 를 붙여야할 까 고민될 때 gitmoji 는 대충 이런 목적이다~ 검색을 하면 그에 맞는 이모지를 추천해줍니다.


Conclusion

사실 Gitmoji 가 뭔지 잘 모르다가 회사에서 사용하면서 알게 되었는데요.

처음에는 CLI 와 GUI 를 왔다갔다 하는게 불편했지만 적응해보니 생각보다 쓸만한 것 같습니다.

특히 Search 기능이 강력해서 적당히 입력해도 알아서 추천해주니 큰 고민 없이 넣을 수 있다는게 좋았습니다.


Reference

'공부 > Git' 카테고리의 다른 글

Git Merge (feat. Github)  (0) 2022.06.20
Git Directory 이름 변경  (0) 2020.04.26

1. Overview

전략 패턴은 여러 알고리즘 (로직) 을 캡슐화 하여 상호 교환 가능하게 하는 패턴입니다.

단순히 패턴만 보면 어떤 목적인지 이해하기 힘들지만 템플릿 메서드 (Template Method) 패턴과 비교해서 생각해보면 됩니다.

템플릿 메서드 패턴은 추상 클래스에 공통된 로직을 놓고 변경되는 로직은 상속을 통해 구현했습니다.

이 패턴의 단점은 부모 클래스에 의존도가 생긴다는 점이었습니다.

전략 패턴은 변하지 않는 부분을 Context 에 두고 변하는 부분을 Strategy 인터페이스의 구현체에 작성합니다.

전략에 해당하는 Strategy 인터페이스와 구현체에는 비즈니스 로직 외에 아무런 로직이 없기 때문에 공통된 로직이 변경되어도 아무런 영향이 없습니다.


2. Strategy Example

간단한 예시 코드를 작성해봅니다.

  • 비즈니스 로직 1 존재
  • 비즈니스 로직 2 존재
  • 각 비즈니스 로직의 실행 시간을 측정하는 공통된 로직 존재

2.1. Before

public class BeforeStrategyApp {

    public static void main(String[] args) {
        logic1();
        logic2();
    }

    private static void logic1() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 1 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }

    private static void logic2() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        System.out.println("비즈니스 로직 2 실행");
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

요구사항을 단순하게 구현하면 이렇게 공통된 로직이 존재합니다.

위 코드에서 다른 부분은 "비즈니스 로직 실행" 하나뿐이고 나머지는 전부 중복된 코드입니다.

전략 패턴을 적용해서 리팩토링 해봅시다.


2.2. Strategy (변경되는 부분)

public interface Strategy {
    void call();
}

public class StrategyLogic1 implements Strategy {

    @Override
    public void call() {
        System.out.println("비즈니스 로직 1 실행");
    }
}

public class StrategyLogic2 implements Strategy {

    @Override
    public void call() {
        System.out.println("비즈니스 로직 2 실행");
    }
}

Strategy 인터페이스와 각 비즈니스 로직을 담당하는 하위 구현체들을 선언합니다.

나중에 비즈니스 로직 3 이 추가된다면 인터페이스나 다른 구현체 변경 없이 새로 추가하기만 하면 됩니다.


2.3. Context (공통된 부분)

public class Context {

    private final Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        strategy.call();
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

공통된 로직이 작성되어 있는 Context 클래스입니다.

Strategy 를 생성자로 받기 때문에 어떤 구현체를 받느냐에 따라 비즈니스 로직이 달라집니다.

비즈니스 로직을 위임 한다고도 표현합니다.


2.4. Application (Client)

public class AfterStrategyApp {

    public static void main(String[] args) {
        Strategy strategy1 = new StrategyLogic1();
        Context context1 = new Context(strategy1);
        context1.execute();

        Strategy strategy2 = new StrategyLogic1();
        Context context2 = new Context(strategy2);
        context2.execute();
    }
}

구현을 원하는 로직에 따라 다른 Strategy 를 생성자로 넘겨줍니다.

Spring 에서는 Bean 주입 설정만 바꾸면 쉽게 로직을 변경할 수 있습니다.


3. 장단점

  • 장점
    • 공통 로직이 부모 클래스에 있지 않고 Context 라는 별도의 클래스에 존재하기 때문에 구현체들에 대한 영향도가 적음
    • ContextStrategy 라는 인터페이스를 의존하고 있기 때문에 구현체를 갈아끼우기 쉬움
  • 단점
    • 로직이 늘어날 때마다 구현체 클래스가 늘어남
    • ContextStrategy 를 한번 조립하면 전략을 변경하기 힘듬

4. Template Callback 패턴

전략 패턴은 생성자 파라미터로 한번 주입하고 나면 동적으로 변경할 수 없다는 단점이 있습니다.

이러한 단점을 극복하기 위해 Context 의 생성자가 아닌 execute() 메서드의 파라미터로 Strategy 를 넘겨주기도 합니다.

이런 패턴을 Template Callback (템플릿 콜백) 패턴이라고도 부릅니다.

템플릿 콜백 패턴도 전략 패턴과 동일하지만 동적으로 비즈니스 로직을 설정할 수 있다는 장점이 있습니다.


5. Template Callback Example

위에서 사용했던 예제를 템플릿 패턴으로 다시 구현해봅니다.


5.1. Callback (Strategy)

public interface Callback {
    void call();
}

전략 패턴과 동일하게 인터페이스 하나를 정의합니다.

실제 비즈니스 로직은 런타임에 넘겨줄거라서 별다른 구현체를 만들지 않아도 됩니다.


5.2. Template (Context)

public class TimeLogTemplate {

    public void execute(Callback callback) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 비즈니스 로직 시작
        callback.call();
        // 비즈니스 로직 종료

        stopWatch.stop();
        System.out.println("실행 시간 = " + stopWatch.getTotalTimeMillis());
    }
}

전략 패턴의 Context 에 해당하는 부분이지만 생성자로 받는 대신 execute() 의 파라미터로 전략을 넘겨받습니다.


5.3. Application (Client)

public class AfterTemplateCallbackApp {

    public static void main(String[] args) {
        TimeLogTemplate timeLogTemplate = new TimeLogTemplate();
        timeLogTemplate.execute(() -> System.out.println("비즈니스 로직 1 실행"));
        timeLogTemplate.execute(() -> System.out.println("비즈니스 로직 2 실행"));
    }
}

Callback 이 함수형 인터페이스라서 람다식으로 간단하게 표현할 수 있습니다.

이제 새로운 로직이 추가되어도 클래스를 만들지 않고 파라미터의 값만 변경해주면 됩니다.

Spring 에서 JdbcTemplate, RestTemplatexxxTemplate 의 형태를 하면 대부분 템플릿 콜백 패턴을 사용한 거라고 생각하시면 됩니다.


Reference

+ Recent posts