InfoGrab DocsInfoGrab Docs

주의 사항(Gotchas)

요약

이 가이드의 목적은 GitLab CE 및 EE 개발 중 기여자가 마주치거나 피해야 할 잠재적인 "gotcha"(함정)를 문서화하는 것입니다. Omnibus GitLab은 에셋 컴파일 후 app/assets 디렉터리를 삭제했습니다.

이 가이드의 목적은 GitLab CE 및 EE 개발 중 기여자가 마주치거나 피해야 할 잠재적인 "gotcha"(함정)를 문서화하는 것입니다.

app/assets 디렉터리에서 파일을 읽지 마세요#

Omnibus GitLab은 에셋 컴파일 후 app/assets 디렉터리를 삭제했습니다. ee/app/assets, vendor/assets 디렉터리도 함께 삭제됩니다.

즉, Omnibus로 설치된 GitLab 인스턴스에서 해당 디렉터리의 파일을 읽으면 실패합니다:

file = Rails.root.join('app/assets/images/logo.svg')

# This file does not exist, read will fail with:
# Errno::ENOENT: No such file or directory @ rb_sysopen
File.read(file)

시퀀스로 생성된 속성의 절댓값에 대해 단언하지 마세요#

다음 factory를 고려해 보세요:

FactoryBot.define do
  factory :label do
    sequence(:title) { |n| "label#{n}" }
  end
end

다음 API spec을 고려해 보세요:

require 'spec_helper'

RSpec.describe API::Labels do
  it 'creates a first label' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end

  it 'creates a second label' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end
end

실행하면 이 spec은 우리가 기대하는 대로 동작하지 않습니다:

1) API::API reproduce sequence issue creates a second label
   Failure/Error: expect(json_response.first['name']).to eq('label1')

     expected: "label1"
          got: "label2"

     (compared using ==)

이는 FactoryBot 시퀀스가 각 예제마다 초기화되지 않기 때문입니다.

시퀀스로 생성된 값은 factory 사용 시 유일성 제약이 있는 속성을 명시적으로 설정하지 않아도 되도록 하기 위해서만 존재한다는 점을 기억하세요.

해결 방법#

시퀀스로 생성된 속성의 값에 대해 단언해야 한다면, 해당 값을 명시적으로 설정해야 합니다. 또한 설정하는 값이 시퀀스 패턴과 일치해서는 안 됩니다.

예를 들어, :label factory를 사용할 때 create(:label, title: 'foo')는 괜찮지만, create(:label, title: 'label1')은 안 됩니다.

다음은 수정된 API spec입니다:

require 'spec_helper'

RSpec.describe API::Labels do
  it 'creates a first label' do
    create(:label, title: 'foo')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('foo')
  end

  it 'creates a second label' do
    create(:label, title: 'bar')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('bar')
  end
end

RSpec에서 expect_any_instance_of 또는 allow_any_instance_of를 사용하지 마세요#

이유#

  • 격리되지 않기 때문에 때로는 동작이 불안정할 수 있습니다.

  • 스텁하려는 메서드가 prepended 모듈에 정의된 경우 동작하지 않는데, 이는 EE에서 흔한 경우입니다. 다음과 같은 오류가 발생할 수 있습니다:

1.1) Failure/Error: expect_any_instance_of(ApplicationSetting).to receive_messages(messages)
     Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.

대안#

대신 다음 중 하나를 사용하세요:

  • expect_next_instance_of

  • allow_next_instance_of

  • expect_next_found_instance_of

  • allow_next_found_instance_of

예를 들어:

# Don't do this:
expect_any_instance_of(Project).to receive(:add_import_job)

# Don't do this:
allow_any_instance_of(Project).to receive(:add_import_job)

다음과 같이 작성할 수 있습니다:

# Do this:
expect_next_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# Do this:
allow_next_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

# Do this:
expect_next_found_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# Do this:
allow_next_found_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

Active Record는 객체를 인스턴스화할 때 모델 클래스의 .new 메서드를 호출하지 않으므로, Active Record 쿼리 및 finder 메서드가 반환하는 객체에 mock을 설정할 때는 expect_next_found_instance_of 또는 allow_next_found_instance_of mock 헬퍼를 사용해야 합니다.

expect_next_found_(number)_instances_ofallow_next_found_(number)_instances_of 헬퍼를 사용하면 동일한 Active Record 모델의 여러 인스턴스에 대해 mock 및 expectation을 설정할 수도 있습니다:

expect_next_found_2_instances_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

allow_next_found_2_instances_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

인스턴스를 특정 인수로 초기화하고 싶다면, 다음과 같이 전달할 수 있습니다:

# Do this:
expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
  expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
end

위 코드는 다음을 기대합니다:

# Above expects:
refresh_service = MergeRequests::RefreshService.new(project, user)
refresh_service.execute(oldrev, newrev, ref)

Exception을 rescue하지 마세요#

“Ruby에서 rescue Exception => e가 나쁜 스타일인 이유는 무엇인가?”를 참조하세요.

이 규칙은 RuboCop에 의해 자동으로 적용됩니다.

뷰에서 인라인 JavaScript를 사용하지 마세요#

인라인 :javascript Haml 필터를 사용하면 성능 오버헤드가 발생합니다. 인라인 JavaScript는 코드를 구조화하는 좋은 방법이 아니므로 피해야 합니다.

이 두 필터는 이니셜라이저에서 제거되었습니다.

추가 참고 자료#

사전 컴파일이 필요 없는 에셋 저장#

사용자에게 제공해야 하는 에셋은 app/assets 디렉터리에 저장되며, 이후 사전 컴파일되어 public/ 디렉터리에 배치됩니다.

그러나 공간 절약 조치로 인해 프로덕션 설치 환경에는 해당 폴더가 포함되지 않으므로, 애플리케이션 코드에서 app/assets 내 파일 내용에 접근할 수 없습니다.

support_bot = Users::Internal.in_organization(organization).support_bot

# accessing a file from the `app/assets` folder
support_bot.avatar = Rails.root.join('app', 'assets', 'images', 'bot_avatars', 'support_bot.png').open

support_bot.save!

위 코드는 로컬 환경에서는 작동하지만, app/assets 폴더가 포함되지 않는 프로덕션 설치 환경에서는 오류가 발생합니다.

해결 방법#

대안은 lib/assets 폴더입니다. 다음 조건을 충족하는 에셋(예: 이미지)을 리포지터리에 추가해야 할 경우 이 폴더를 사용하세요:

  • 에셋을 사용자에게 직접 제공할 필요가 없는 경우(따라서 사전 컴파일이 필요 없는 경우).

  • 에셋을 애플리케이션 코드를 통해 접근해야 하는 경우.

요약하면:

최종 사용자에게 사전 컴파일되어 제공되어야 하는 에셋은 app/assets를 사용하세요. 최종 사용자에게 직접 제공할 필요는 없지만 애플리케이션 코드에서 접근이 필요한 에셋은 lib/assets를 사용하세요.

참고 MR: !37671

has_many through: 또는 has_one through: 연관을 오버라이드하지 마세요#

:through 옵션이 있는 연관은 오버라이드하면 잘못된 객체가 실수로 삭제될 수 있으므로 오버라이드하면 안 됩니다.

이는 destroy() 메서드가 has_many through:has_one through: 연관에서 다르게 동작하기 때문입니다.

group.users.destroy(id)

위 코드 예제는 User 레코드를 삭제하는 것처럼 읽히지만, 실제로는 Member 레코드를 삭제합니다. 이는 users 연관이 Group에서 has_many through: 연관으로 정의되어 있기 때문입니다:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members
end

그리고 Rails는 이러한 연관에서 destroy()를 사용할 때 다음과 같은 동작을 합니다:

:through 옵션이 사용되면 객체 자체가 아닌 join 레코드가 삭제됩니다.

이것이 UserGroup을 연결하는 join 레코드인 Member 레코드가 삭제되는 이유입니다.

이제 users 연관을 다음과 같이 오버라이드하면:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members

  def users
    super.where(admin: false)
  end
end

오버라이드된 메서드는 destroy()의 위 동작을 변경하여 다음을 실행하면:

group.users.destroy(id)

User 레코드가 삭제되며, 이는 데이터 손실로 이어질 수 있습니다.

요약하면, has_many through: 또는 has_one through: 연관을 오버라이드하는 것은 위험할 수 있습니다. 이를 방지하기 위해 !131455에서 자동화된 검사를 도입하고 있습니다.

자세한 내용은 이슈 424536을 참조하세요.

주의 사항(Gotchas)

GitLab v19.1
원문 보기
요약

이 가이드의 목적은 GitLab CE 및 EE 개발 중 기여자가 마주치거나 피해야 할 잠재적인 "gotcha"(함정)를 문서화하는 것입니다. Omnibus GitLab은 에셋 컴파일 후 app/assets 디렉터리를 삭제했습니다.

이 가이드의 목적은 GitLab CE 및 EE 개발 중 기여자가 마주치거나 피해야 할 잠재적인 "gotcha"(함정)를 문서화하는 것입니다.

app/assets 디렉터리에서 파일을 읽지 마세요#

Omnibus GitLab은 에셋 컴파일 후 app/assets 디렉터리를 삭제했습니다. ee/app/assets, vendor/assets 디렉터리도 함께 삭제됩니다.

즉, Omnibus로 설치된 GitLab 인스턴스에서 해당 디렉터리의 파일을 읽으면 실패합니다:

file = Rails.root.join('app/assets/images/logo.svg')

# This file does not exist, read will fail with:
# Errno::ENOENT: No such file or directory @ rb_sysopen
File.read(file)

시퀀스로 생성된 속성의 절댓값에 대해 단언하지 마세요#

다음 factory를 고려해 보세요:

FactoryBot.define do
  factory :label do
    sequence(:title) { |n| "label#{n}" }
  end
end

다음 API spec을 고려해 보세요:

require 'spec_helper'

RSpec.describe API::Labels do
  it 'creates a first label' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end

  it 'creates a second label' do
    create(:label)

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('label1')
  end
end

실행하면 이 spec은 우리가 기대하는 대로 동작하지 않습니다:

1) API::API reproduce sequence issue creates a second label
   Failure/Error: expect(json_response.first['name']).to eq('label1')

     expected: "label1"
          got: "label2"

     (compared using ==)

이는 FactoryBot 시퀀스가 각 예제마다 초기화되지 않기 때문입니다.

시퀀스로 생성된 값은 factory 사용 시 유일성 제약이 있는 속성을 명시적으로 설정하지 않아도 되도록 하기 위해서만 존재한다는 점을 기억하세요.

해결 방법#

시퀀스로 생성된 속성의 값에 대해 단언해야 한다면, 해당 값을 명시적으로 설정해야 합니다. 또한 설정하는 값이 시퀀스 패턴과 일치해서는 안 됩니다.

예를 들어, :label factory를 사용할 때 create(:label, title: 'foo')는 괜찮지만, create(:label, title: 'label1')은 안 됩니다.

다음은 수정된 API spec입니다:

require 'spec_helper'

RSpec.describe API::Labels do
  it 'creates a first label' do
    create(:label, title: 'foo')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('foo')
  end

  it 'creates a second label' do
    create(:label, title: 'bar')

    get api("/projects/#{project.id}/labels", user)

    expect(response).to have_gitlab_http_status(:ok)
    expect(json_response.first['name']).to eq('bar')
  end
end

RSpec에서 expect_any_instance_of 또는 allow_any_instance_of를 사용하지 마세요#

이유#

  • 격리되지 않기 때문에 때로는 동작이 불안정할 수 있습니다.

  • 스텁하려는 메서드가 prepended 모듈에 정의된 경우 동작하지 않는데, 이는 EE에서 흔한 경우입니다. 다음과 같은 오류가 발생할 수 있습니다:

1.1) Failure/Error: expect_any_instance_of(ApplicationSetting).to receive_messages(messages)
     Using `any_instance` to stub a method (elasticsearch_indexing) that has been defined on a prepended module (EE::ApplicationSetting) is not supported.

대안#

대신 다음 중 하나를 사용하세요:

  • expect_next_instance_of

  • allow_next_instance_of

  • expect_next_found_instance_of

  • allow_next_found_instance_of

예를 들어:

# Don't do this:
expect_any_instance_of(Project).to receive(:add_import_job)

# Don't do this:
allow_any_instance_of(Project).to receive(:add_import_job)

다음과 같이 작성할 수 있습니다:

# Do this:
expect_next_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# Do this:
allow_next_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

# Do this:
expect_next_found_instance_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

# Do this:
allow_next_found_instance_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

Active Record는 객체를 인스턴스화할 때 모델 클래스의 .new 메서드를 호출하지 않으므로, Active Record 쿼리 및 finder 메서드가 반환하는 객체에 mock을 설정할 때는 expect_next_found_instance_of 또는 allow_next_found_instance_of mock 헬퍼를 사용해야 합니다.

expect_next_found_(number)_instances_ofallow_next_found_(number)_instances_of 헬퍼를 사용하면 동일한 Active Record 모델의 여러 인스턴스에 대해 mock 및 expectation을 설정할 수도 있습니다:

expect_next_found_2_instances_of(Project) do |project|
  expect(project).to receive(:add_import_job)
end

allow_next_found_2_instances_of(Project) do |project|
  allow(project).to receive(:add_import_job)
end

인스턴스를 특정 인수로 초기화하고 싶다면, 다음과 같이 전달할 수 있습니다:

# Do this:
expect_next_instance_of(MergeRequests::RefreshService, project, user) do |refresh_service|
  expect(refresh_service).to receive(:execute).with(oldrev, newrev, ref)
end

위 코드는 다음을 기대합니다:

# Above expects:
refresh_service = MergeRequests::RefreshService.new(project, user)
refresh_service.execute(oldrev, newrev, ref)

Exception을 rescue하지 마세요#

“Ruby에서 rescue Exception => e가 나쁜 스타일인 이유는 무엇인가?”를 참조하세요.

이 규칙은 RuboCop에 의해 자동으로 적용됩니다.

뷰에서 인라인 JavaScript를 사용하지 마세요#

인라인 :javascript Haml 필터를 사용하면 성능 오버헤드가 발생합니다. 인라인 JavaScript는 코드를 구조화하는 좋은 방법이 아니므로 피해야 합니다.

이 두 필터는 이니셜라이저에서 제거되었습니다.

추가 참고 자료#

사전 컴파일이 필요 없는 에셋 저장#

사용자에게 제공해야 하는 에셋은 app/assets 디렉터리에 저장되며, 이후 사전 컴파일되어 public/ 디렉터리에 배치됩니다.

그러나 공간 절약 조치로 인해 프로덕션 설치 환경에는 해당 폴더가 포함되지 않으므로, 애플리케이션 코드에서 app/assets 내 파일 내용에 접근할 수 없습니다.

support_bot = Users::Internal.in_organization(organization).support_bot

# accessing a file from the `app/assets` folder
support_bot.avatar = Rails.root.join('app', 'assets', 'images', 'bot_avatars', 'support_bot.png').open

support_bot.save!

위 코드는 로컬 환경에서는 작동하지만, app/assets 폴더가 포함되지 않는 프로덕션 설치 환경에서는 오류가 발생합니다.

해결 방법#

대안은 lib/assets 폴더입니다. 다음 조건을 충족하는 에셋(예: 이미지)을 리포지터리에 추가해야 할 경우 이 폴더를 사용하세요:

  • 에셋을 사용자에게 직접 제공할 필요가 없는 경우(따라서 사전 컴파일이 필요 없는 경우).

  • 에셋을 애플리케이션 코드를 통해 접근해야 하는 경우.

요약하면:

최종 사용자에게 사전 컴파일되어 제공되어야 하는 에셋은 app/assets를 사용하세요. 최종 사용자에게 직접 제공할 필요는 없지만 애플리케이션 코드에서 접근이 필요한 에셋은 lib/assets를 사용하세요.

참고 MR: !37671

has_many through: 또는 has_one through: 연관을 오버라이드하지 마세요#

:through 옵션이 있는 연관은 오버라이드하면 잘못된 객체가 실수로 삭제될 수 있으므로 오버라이드하면 안 됩니다.

이는 destroy() 메서드가 has_many through:has_one through: 연관에서 다르게 동작하기 때문입니다.

group.users.destroy(id)

위 코드 예제는 User 레코드를 삭제하는 것처럼 읽히지만, 실제로는 Member 레코드를 삭제합니다. 이는 users 연관이 Group에서 has_many through: 연관으로 정의되어 있기 때문입니다:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members
end

그리고 Rails는 이러한 연관에서 destroy()를 사용할 때 다음과 같은 동작을 합니다:

:through 옵션이 사용되면 객체 자체가 아닌 join 레코드가 삭제됩니다.

이것이 UserGroup을 연결하는 join 레코드인 Member 레코드가 삭제되는 이유입니다.

이제 users 연관을 다음과 같이 오버라이드하면:

class Group < Namespace
  has_many :group_members, -> { where(requested_at: nil).where.not(members: { access_level: Gitlab::Access::MINIMAL_ACCESS }) }, dependent: :destroy, as: :source

  has_many :users, through: :group_members

  def users
    super.where(admin: false)
  end
end

오버라이드된 메서드는 destroy()의 위 동작을 변경하여 다음을 실행하면:

group.users.destroy(id)

User 레코드가 삭제되며, 이는 데이터 손실로 이어질 수 있습니다.

요약하면, has_many through: 또는 has_one through: 연관을 오버라이드하는 것은 위험할 수 있습니다. 이를 방지하기 위해 !131455에서 자동화된 검사를 도입하고 있습니다.

자세한 내용은 이슈 424536을 참조하세요.